Simple implementation of recoil’s state subscription sharing

Time:2021-1-19

Recoil is a new react state management library, which is still in the experimental stage. It proposes decentralized atomic state management, provides hooks API for setting and obtaining state, and makes components subscribe to state. This paper simply implements the principle of making multiple components share and subscribe to a state in recoil.

my-recoil (v1.0)

I have written an article about how to use recoil and the principle of recoil. You can go tohere

To implement a small recoil, call it my recoil

Direct code:

// my-recoil
import React,{ useEffect, useState, useRef, useContext } from 'react';

const nodes = new Map()
const subNodes = new Map()
let subID = 0

class Node{
  constructor(k, v){
    this.key = k
    this.value = v
  }
  getValue(){
    return this.value 
  }
  setValue(newV) {
    this.value = newV
  }
}

export function useMySetRecoilState(atom) {
  const { key, defaultValue } = atom
  let node
  const store = useStoreRef().current
  const hasNode = store.atomValues.has(key)
  if (hasNode) {
    node = store.atomValues.get(key)
  } else {
    const newNode = new Node(key, defaultValue)
    store.atomValues.set(key, newNode)
    node = store.atomValues.get(key)
  }

  const setState = (newValueOrUpdater) => {
    let newValue
    if (typeof newValueOrUpdater === 'function') {
      newValue = newValueOrUpdater(node.getValue())
    }
    node.setValue(newValue)
    store.atomValues.set(key, node)
    store.replaceState()
  }

  return setState
}

function subRecoilState(store, atomkey, subid, cb) {
  if(!store.nodeToComponentSubscriptions.has(`${subid}-${atomkey}`)){
    store.nodeToComponentSubscriptions.set(`${subid}-${atomkey}`, cb)
  }
}

export function useMyRecoilValue(atom) {
  const [_, forceUpdate] = useState([])
  const { key, defaultValue } = atom
  const storeRef = useStoreRef()
  const store = storeRef.current
  let hasNode = store.atomValues.has(key)
  let node
  if (!hasNode) {
    node = new Node(key, defaultValue)
    store.atomValues.set(key, node)
  }
  node = store.atomValues.get(key)

  useEffect(() => {
    subRecoilState(store, key, subID++, () =>{
      forceUpdate([])
    })
  }, [key, node, store, storeRef])
  
  return node.getValue()
}

export function useMyRecoilState(atom) {
  return [useMyRecoilValue(atom), useMySetRecoilState(atom)]
}

const storeContext = React.createContext()
export const useStoreRef = () => useContext(storeContext)

export default function MyRecoilRoot({children}) {
  const notifyUpdate = useRef()
  function setNotify(x) {
    notifyUpdate.current = x
  }
  function Batcher({setNotify}) {
    const [_, setState] = useState([])
    setNotify(() => setState([]))

    useEffect(() => {
      //Broadcast update events
      storeState.current.nodeToComponentSubscriptions.forEach((cb) => {
        cb()
      })
    })
    return null
  }
  function replaceState(key) {
    notifyUpdate.current()
    storeState.current.updateAtomKey = key
  }
  const storeState = useRef({
    atomValues: nodes,
    replaceState,
    nodeToComponentSubscriptions: subNodes
  })

  return <div>
    <storeContext.Provider value={storeState}>
      <Batcher setNotify={setNotify}/>
      {children}
    </storeContext.Provider>
  </div>
}

Then let’s use the my recoil library, define three react components in another file, and use the MyRecoilRootuseMyRecoilStateuseMyRecoilValueTo simulate recoil’sRecoilRootuseRecoilStateuseRecoilValue

import React from 'react'
import MyRecoilRoot, {useMyRecoilState, useMyRecoilValue} from './recoil'

const countAtom = {
  key: 'count_atom',
  defaultValue: 0
}


const style = {border: 'solid 1px #456', width: '200px', margin: '20px'}
//Com1.whyDidYouRender = true
function Com1() {
  const [count, setCount] = useMyRecoilState(countAtom)

  function handleChange(){
    setCount(count => count + 1)
  }

  return (
    <div style={style}>
      <h2>Component 1</h2>
      <div>count: {count}</div>
      <div>count1: {count1}</div>
      < button onclick = {handlechange} > Click update count to see if components 2 and 3 will be updated < / button >
    </div>
  )
}

Com2.whyDidYouRender = true
function Com2() {
  const count = useMyRecoilValue(countAtom)
  return (
    <div style={style}>
      <h2>Component 2</h2>
      <div>count: {count}</div>
    </div>
  )
}

//Com3.whyDidYouRender = true
function Com3() {
  const count = useMyRecoilValue(countAtom)
  return (
    <div style={style}>
      <h2>Component 3</h2>
      <div>count: {count}</div>
    </div>
  )
}

App.whyDidYouRender = true
export default function App() {
  
  return <MyRecoilRoot>
    <Com1/>
    <Com2/>
    <Com3/>
  </MyRecoilRoot>
}

COM1, com2 and COM3 will subscribe to countatom. When the value of countatom is changed in COM1, com2 and COM3 will receive change notification and update the component. As follows, we click the button of COM1 to update the value of countatom. Com2 and COM3 will also receive a notification to trigger re render

Simple implementation of recoil's state subscription sharing

Now let’s explain the principle of my recoil.

First of all, define a myrecoil root component and use context to package the sub components. Here, we define the store on context and define a usestoreref:

export const useStoreRef = () => useContext(storeContext)

In this way, the subcomponent can use usestoreref to get the store. Whenever we use usemyrecoilvalue (someatom) in a component, we will use usestate to define an empty state and return a forceupdate. As long as we call forceupdate, the update will be triggered and the value of someatom in the store will be retrieved. The logic of subscribing to trigger the update event is defined by subrecoilstate, and subrecoilstate will define a value in the storenodeToComponentSubscriptionsFor each call useMyRecoilValue(someAtom) The generated forceupdate is placed in thenodeToComponentSubscriptionsAbove, wait until the calluseMySetRecoilState(someAtom)To set the value of someatom, the batchersetState([]), the batcher is triggered to update, so the useeffect will execute the following code:

useEffect(() => {
      //Broadcast update events
      storeState.current.nodeToComponentSubscriptions.forEach((cb) => {
        cb()
      })
    })

Take out the forceupdate in nodetocomponentsubscriptions to execute, that is, trigger the update of usemyrecoilvalue (someatom) to get the new state, and then trigger the component update. In this way, the component subscribes to the store state.

Update on demand

The above is the case of subscribing to a state. If multiple components subscribe to multiple states, for example, two components a and B subscribe to Statea and stateb respectively, then according to the above update event broadcast mechanism, when Statea is updated in component A, the update events will be broadcast to a and B indiscriminately, resulting in both components being updated, but B does not need to be updated.

To solve this problem, a key can be added to the subscription event to classify the update events according to the subscribed state. Therefore, only when Statea is updated, the component subscribing to stateb will not receive the update notification. According to this idea, reconstruct my recoil:

// my-recoil

import React,{ useEffect, useState, useRef, useContext } from 'react';

const nodes = new Map()
const subNodes = new Map()
let subID = 0

class Node{
  constructor(k, v){
    this.key = k
    this.value = v
  }
  getValue(){
    return this.value 
  }
  setValue(newV) {
    this.value = newV
  }
}

export function useMySetRecoilState(atom) {
  const { key, defaultValue } = atom
  let node
  const store = useStoreRef().current
  const hasNode = store.atomValues.has(key)
  if (hasNode) {
    node = store.atomValues.get(key)
  } else {
    const newNode = new Node(key, defaultValue)
    store.atomValues.set(key, newNode)
    node = store.atomValues.get(key)
  }

  const setState = (newValueOrUpdater) => {
    let newValue
    if (typeof newValueOrUpdater === 'function') {
      newValue = newValueOrUpdater(node.getValue())
    }
    node.setValue(newValue)
    store.atomValues.set(key, node)
    store.replaceState(key)
  }

  return setState
}

function subRecoilState(store, atomkey, subid, cb) {
  if(!store.nodeToComponentSubscriptions.has(atomkey)) {
    store.nodeToComponentSubscriptions.set(atomkey, new Map())
  }
  if(!store.nodeToComponentSubscriptions.get(atomkey).has(subid)){
    store.nodeToComponentSubscriptions.get(atomkey).set(subid, cb)
  }
}

export function useMyRecoilValue(atom) {
  const [_, forceUpdate] = useState([])
  const { key, defaultValue } = atom
  const storeRef = useStoreRef()
  const store = storeRef.current
  let hasNode = store.atomValues.has(key)
  let node
  if (!hasNode) {
    node = new Node(key, defaultValue)
    store.atomValues.set(key, node)
  }
  node = store.atomValues.get(key)

  useEffect(() => {
    subRecoilState(store, key, subID++, () =>{
      forceUpdate([])
    })
  }, [key, node, store, storeRef])
  
  return node.getValue()
}

export function useMyRecoilState(atom) {
  return [useMyRecoilValue(atom), useMySetRecoilState(atom)]
}

const storeContext = React.createContext()
export const useStoreRef = () => useContext(storeContext)

export default function MyRecoilRoot({children}) {
  const notifyUpdate = useRef()
  function setNotify(x) {
    notifyUpdate.current = x
  }
  function Batcher({setNotify}) {
    const [_, setState] = useState([])
    setNotify(() => setState([]))

    useEffect(() => {
      //Broadcast update events
      const { updateAtomKey } = storeState.current
      storeState.current.nodeToComponentSubscriptions.has(updateAtomKey) &&
      storeState.current.nodeToComponentSubscriptions.get(updateAtomKey).forEach((cb) => {
        cb()
      })
    })
    return null
  }
  function replaceState(key) {
    notifyUpdate.current()
    storeState.current.updateAtomKey = key
  }
  const storeState = useRef({
    atomValues: nodes,
    replaceState,
    nodeToComponentSubscriptions: subNodes,
    updateAtomKey: null
  })

  return <div>
    <storeContext.Provider value={storeState}>
      <Batcher setNotify={setNotify}/>
      {children}
    </storeContext.Provider>
  </div>
}

As you can see, we add an updateatomkey to the store. When we call useresoilsetstate to set, we will assign the key of the atom to be set to the updateatomkey. When we broadcast the update event, according to the updateatomkey, we get and execute the callback that triggers the update, and finally implement the on-demand update. The logic of subscribing to subrecoilstate and broadcasting update events in the code is as follows:

subRecoilState:

function subRecoilState(store, atomkey, subid, cb) {
  if(!store.nodeToComponentSubscriptions.has(atomkey)) {
    store.nodeToComponentSubscriptions.set(atomkey, new Map())
  }
  if(!store.nodeToComponentSubscriptions.get(atomkey).has(subid)){
    store.nodeToComponentSubscriptions.get(atomkey).set(subid, cb)
  }
}

Batcher:

function Batcher({setNotify}) {
    const [_, setState] = useState([])
    setNotify(() => setState([]))

    useEffect(() => {
      //Broadcast update events
      const { updateAtomKey } = storeState.current
      storeState.current.nodeToComponentSubscriptions.has(updateAtomKey) &&
      storeState.current.nodeToComponentSubscriptions.get(updateAtomKey).forEach((cb) => {
        cb()
      })
    })
    return null
  }

Now let’s demonstrate that COUNT1 is added to COM1. COM1, com2 and COM3 subscribe to count, COM1 subscribe to COUNT1, and com2 and COM3 do not subscribe to COUNT1. When we modify count in COM1, the components COM1, com2 and COM3 will be updated; while when we modify COUNT1 in COM1, only COM1, com2 and COM3 will not be updated.

This is the general logic of implementing subscription and sharing state in recoil.

Recommended Today

How to Build a Cybersecurity Career

Original text:How to Build a Cybersecurity Career How to build the cause of network security Normative guidelines for building a successful career in the field of information security fromDaniel miesslerstayinformation safetyCreated / updated: December 17, 2019 I’ve been doing itinformation safety(now many people call it network security) it’s been about 20 years, and I’ve spent […]