-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Don't work with react 18.2 #109
Comments
That's the intended behavior by design. |
I have more than 1000 components with useContextSeletor. Any changes in context value trigger call render function of all components. Even if it doesn't cause effects and update the DOM(only call render function), it takes a lot of computing resources. I change example and add Redux example: https://codesandbox.io/s/muddy-bush-ndqqwg?file=/src/App.js
100 components:
|
one more example - symetric updates states + calc render time, 1000 nodes: https://codesandbox.io/s/muddy-bush-ndqqwg?file=/src/App.js stats:
|
One workaround is to Another would be to use subscription based state management such as Redux / Zustand / ... You can also implement a simplified useContextSelector without concurrency support pretty easily. Seel also #100 |
import {
createContext as createContextOrig,
useContext as useContextOrig,
useRef,
useSyncExternalStore,
} from 'react';
export const createContext = (defaultValue) => {
const context = createContextOrig();
const ProviderOrig = context.Provider;
context.Provider = ({ value, children }) => {
const storeRef = useRef();
let store = storeRef.current;
if (!store) {
const listeners = new Set();
store = {
value,
subscribe: (l) => { listeners.add(l); return () => listeners.delete(l); },
notify: () => listeners.forEach((l) => l()),
}
storeRef.current = store;
}
useEffect(() => {
if (!Object.is(store.value, value)) {
store.value = value;
store.notify();
}
});
return <ProviderOrig value={store}>{children}</ProviderOrig>
};
return context;
}
export const useContextSelector = (context, selector) => {
const store = useContextOrig(context);
return useSyncExternalStore(
store.subscribe,
() => selector(store.value),
);
}; |
This works in React 18.2 and even works with |
Great to hear that, because I haven't tried it. 😁 |
However, |
I tried to add a cache with export function useContextSelector<T, Selected>(
context: React.Context<ContextStore<T>>,
selector: (value: T) => Selected,
) {
const store = useContext(context);
let cache: any;
return useSyncExternalStore(store.subscribe, () => {
const value = selector(store.value);
if (!shallowEqual(cache, value)) {
cache = value;
}
return cache;
});
} |
Use https://www.npmjs.com/package/use-sync-external-store instead and pass |
I've found that the |
Here's a cleaned up (imo) version of the (vanilla js) solution aboveimport {
createContext,
useContext,
useEffect,
useRef,
useSyncExternalStore,
} from "react";
export const createContextWithSelectors = (defaultValue) => {
const context = createContext(defaultValue);
const OldProvider = context.Provider;
const NewProvider = ({ value, children }) => {
const store = useRef();
const subscribers = useRef(new Set());
if (!store.current) {
store.current = {
value,
subscribe: (subscriber) => {
subscribers.current.add(subscriber);
return () => subscribers.current.delete(subscriber);
},
};
}
useEffect(() => {
if (!Object.is(store.value, value)) {
store.value = value;
subscribers.current.forEach((subscriber) => subscriber());
}
});
return <OldProvider value={store.current}>{children}</OldProvider>;
};
context.Provider = NewProvider;
return context;
};
export const useContextSelector = (context, selector) => {
const store = useContext(context);
return useSyncExternalStore(store.subscribe, () => selector(store.value));
}; Note that One thing I'm noticing is that if you do something like this: const { saving } = useContextSelector(SomeContext, (state) => ({ saving: state.saving })); you get a stack overflow. I guess this is because the object you're returning from the selector is different (referentially) every time. Assuming that's the case, you could probably incorporate something like Relatedly, @Jayatubi, if you want to check shallowly, wouldn't the place to do it be where the |
And here is a typescript version. Please let me know if you find issues. Getting rid of the import {
createContext,
useContext,
useEffect,
useRef,
useSyncExternalStore,
type Context,
type Provider,
} from "react";
/** https://github.com/dai-shi/use-context-selector/issues/109 */
type Subscribe = Parameters<typeof useSyncExternalStore>[0];
type Subscriber = () => void;
type Store<Value> = { value: Value; subscribe: Subscribe };
/** useContext that supports selectors */
export const createContextWithSelectors = <Value,>(defaultValue: Value) => {
/** create default react context */
const context = createContext<Store<Value>>({
/** put context value one level deep to allow addition of... */
value: defaultValue,
/** subscribe function to access from outside */
subscribe: () => () => {},
});
/** original provider */
const Original = context.Provider;
/** provider to replace original one */
const NewProvider: CallSignature<Provider<Value>> = ({ value, children }) => {
/** non-reactive store */
const store = useRef<Store<Value>>(null);
/** subscribers of store */
const subscribers = useRef(new Set<Subscriber>());
/** initialize store */
if (!store.current)
store.current = {
value,
subscribe: (subscriber) => {
subscribers.current.add(subscriber);
return () => subscribers.current.delete(subscriber);
},
};
/** when context value changes */
useEffect(() => {
if (!store.current) return;
/** if new value is referentially different from old value */
if (!Object.is(store.current.value, value)) {
/** update value */
store.current.value = value;
/** notify subscribers of change */
subscribers.current.forEach((subscriber) => subscriber());
}
}, [value]);
/** render original provider */
return <Original value={store.current}>{children}</Original>;
};
/** replace old provider with new one */
context.Provider = NewProvider as Provider<Store<Value>>;
return context as unknown as ReturnType<typeof createContext<Value>>;
};
/** select slice from context value */
export const useContextSelector = <Value, Slice>(
context: Context<Value>,
selector: (value: Value) => Slice,
) => {
const store = useContext(context) as Store<Value>;
return useSyncExternalStore<Slice>(store.subscribe, () =>
selector(store.value),
);
};
/** https://stackoverflow.com/questions/58657325/typescript-pick-call-signature-from-function-interface */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type CallSignature<T> = T extends (...args: any[]) => any
? (...args: Parameters<T>) => ReturnType<T>
: never; And if you then want to make a helper function for a particular context: type SomeState = { a: 1, b: 2, c:3 };
export const SomeContext = createContextWithSelectors<SomeState>({} as SomeState);
export const useSomeState = <Key extends keyof SomeState>(key: Key) =>
useContextSelector(SomeContext, (someState) => someState[key]);
useSomeState("a"); // -> 1 (Sorry for the comment spam, wanted to split them up by topic/content). |
Can anyone try |
Components used useContextSelector with return save value always rerenders.
Example https://codesandbox.io/s/musing-poincare-hz2g6g?file=/src/App.js
The text was updated successfully, but these errors were encountered: