Last active
September 18, 2023 20:07
-
-
Save romgrk/a3dc5d3de5c011e0925b2f2aee14ee98 to your computer and use it in GitHub Desktop.
Minimal react store with selector
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Usage: | |
| * | |
| * const abContext = Store.createContext() | |
| * | |
| * function Parent(props: { a: number, b: number }) { | |
| * return ( | |
| * <Store.Provider context={abContext} value={props}> | |
| * <ChildrenA> | |
| * <ChildrenB> | |
| * </Store.Provider> | |
| * ) | |
| * } | |
| * | |
| * function ChildrenA() { | |
| * const a = useStoreSelector(abContext, store => store.a) | |
| * return ( | |
| * <div>{a}</div> | |
| * ) | |
| * } | |
| * | |
| * function ChildrenB() { | |
| * const b = useStoreSelector(abContext, store => store.b) | |
| * return ( | |
| * <div>{b}</div> | |
| * ) | |
| * } | |
| * | |
| */ | |
| import React from 'react'; | |
| import useLazyRef from './useLazyRef'; | |
| type Store<T> = { | |
| value: T, | |
| listeners: Set<(value: T) => void>, | |
| } | |
| export function createContext<T>() { | |
| return React.createContext<Store<T>>(undefined as any); | |
| } | |
| function createStore<T>(value: T): Store<T> { | |
| return { | |
| value, | |
| listeners: new Set(), | |
| } | |
| } | |
| export function Provider<T>({ context, value, children }: { | |
| context: React.Context<Store<T>>, | |
| value: T, | |
| children: React.ReactNode | |
| }) { | |
| const store = useLazyRef(createStore, value); | |
| React.useEffect(() => { | |
| if (store.current.value === value) { | |
| return; | |
| } | |
| store.current.value = value; | |
| store.current.listeners.forEach(listener => { | |
| listener(value); | |
| }) | |
| }) | |
| return ( | |
| <context.Provider value={store.current}> | |
| {children} | |
| </context.Provider> | |
| ) | |
| } | |
| const NEVER = [] as unknown[]; | |
| export function useStoreSelector<T, U>( | |
| context: React.Context<Store<T>>, | |
| selector: (value: T) => U, | |
| ) { | |
| const store = React.useContext(context) | |
| const [state, setState] = React.useState<U>(selector(store.value)) | |
| React.useEffect(() => { | |
| const handleUpdate = (value: T) => { | |
| setState(selector(value)); | |
| } | |
| store.listeners.add(handleUpdate) | |
| return () => { | |
| store.listeners.delete(handleUpdate) | |
| } | |
| }, NEVER) | |
| return state; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The content makes sense to me.
The only question I would have is whether can we fall into a multi-rendering tree case? I mean, if
Parentrerender, it's going to try to renderChildrenA. At the same time, when Parent's contextvalue, he's going to try to render ChildrenA'suseStoreSelector. How is React able to not render to batch these render signals to not render twice?