I have a basic react context, similar to below:
function IdProvider({ children }: any) {
const [id, setId] = useState("DEFAULT_ID")
return (
<IdContext.Provider value={{ id, setId }}>
{children}
</IdContext.Provider>
)
}
I'm wrapping all of my routes with this provider, and have one component as below which I want to use to update the Id:
function UpdateForm() {
const { id, setId } = useId() // wrapper for the IdContext
const moveToDisplay = (newId: string) => {
setId(newId)
window.location.href = "/display/" + id
}
return (
<>
<span onClick={() => moveToDisplay("NEW_ID")}>
Update and redirect
</span>
</>
)
}
Upon redirecting, this component is used:
function DisplayId(): JSX.Element {
const { id } = useId()
useEffect(() => {
document.title = id
}, [id])
return (
<>
{id}
</>
)
}
The issue is, the initial setId(newId) doesn't update the state, so when window.location.href = "/display/" + id is called it redirects to /display/DEFAULT_ID, and the DisplayId component still uses the DEFAULT_ID value when rendering. From my understanding the useState is asynchronous, so I'm not entirely sure how I should approach this problem. I've tried adding a 5 second timeout after setId is called, but the value of id is still the default value.
EDIT - SOLVED
Figured out the issue and it was fairly unrelated. I was using a single constant (e.g. DEFAULT_ID) to initialise state in various places, without realising that React checks for referential equality when updating / re-rendering.
useContext provides the ability to pass properties through the react chain of components. So change your provider to this & make sure it's exporting
<IdContext.Provider value={id}>
then in your child you can update that by importing the Context:
import IdContext from './IdProvider'
and useContext:
const [context, setContext] = useContext(IdContext)
then set it with the new value:
const moveToDisplay = (newId: string) => {
setContext(newId)
window.location.href = "/display/" + newId
}
After that you import it in the same fashion through your DisplayId and just use the value
I am using the Context API to load categories from an API. This data is needed in many components, so it's suitable to use context for this task.
The categories can be expanded in one of the child components, by using a form. I would like to be able to tell useCategoryLoader to reload once a new category gets submitted by one of the child components. What is the best practice in this scenario? I couldn't really find anything on google with the weird setup that I have.
I tried to use a state in CategoryStore, that holds a boolean refresh State which gets passed as Prop to the callback and can be modified by the child components. But this resulted in a ton of requests.
This is my custom hook useCategoryLoader.ts to load the data:
import { useCallback } from 'react'
import useAsyncLoader from '../useAsyncLoader'
import { Category } from '../types'
interface Props {
date: string
}
interface Response {
error?: Error
loading?: boolean
categories?: Array<Category>
}
const useCategoryLoader = (date : Props): Response => {
const { data: categories, error, loading } = useAsyncLoader(
// #ts-ignore
useCallback(() => {
return *APICALL with modified date*.then(data => data)
}, [date])
)
return {
error,
loading,
categories
}
}
export default useCategoryLoader
As you can see I am using useCallback to modify the API call when input changes. useAsyncloaderis basically a useEffect API call.
Now this is categoryContext.tsx:
import React, { createContext, FC } from 'react'
import { useCategoryLoader } from '../api'
import { Category } from '../types'
// ================================================================================================
const defaultCategories: Array<Category> = []
export const CategoryContext = createContext({
loading: false,
categories: defaultCategories
})
// ================================================================================================
const CategoryStore: FC = ({ children }) => {
const { loading, categories } = useCategoryLoader({date})
return (
<CategoryContext.Provider
value={{
loading,
topics
}}
>
{children}
</CategoryContext.Provider>
)
}
export default CategoryStore
I'm not sure where the variable date comes from in CategoryStore. I'm assuming that this is an incomplete attempt to force refreshes based on a timestamp? So let's complete it.
We'll add a reload property to the context.
export const CategoryContext = createContext({
loading: false,
categories: defaultCategories,
reload: () => {},
})
We'll add a state which stores a date timestamp to the CategoryStore and create a reload function which sets the date to the current timestamp, which should cause the loader to refresh its data.
const CategoryStore: FC = ({ children }) => {
const [date, setDate] = useState(Date.now().toString());
const { loading = true, categories = [] } = useCategoryLoader({ date });
const reload = useCallback(() => setDate(Date.now.toString()), []);
return (
<CategoryContext.Provider
value={{
loading,
categories,
reload
}}
>
{children}
</CategoryContext.Provider>
)
}
I think that should work. The part that I am most iffy about is how to properly memoize a function that depends on Date.now().
I want to create custom hook to remove a lot of boilerplate from reusable code.
My redux setup involves a bunch of combined reducers so getting redux values using useSelector from react-redux entails quite a lot of boilerplate code.
Let's say I have an admin reducer inside my rootReducer. I could get one of it's values as follows:
const MyComponent: FC = () => {
const value = useSelector<IRootReducer, boolean>(
({ admin: { val1 } }) => val1
)
// rest of code
}
I want to create a custom hook useAdminSelector based off the implementation above so that it can be used as follows:
const value = useAdminSelector<boolean>(({ val1 }) => val1)
In the definition of the implementation I'd like to have my IAdminReducer interface implemented too.
Here's my attempt:
export function useApplicantSelector<T>((applicantState: IApplicant): T => (retVal)): T {
return useSelector<IReducer, T>((state) => (state.applicant))
}
But this solution is obviously syntactically incorrect.
How about
export const useAdminSelector = <T>(adminSelector: (adminState: IAdminReducer) => T) => {
return useSelector((state: IRootReducer) => adminSelector(state.admin))
}
and then
const value = useAdminSelector<boolean>(({ booleanVal }) => val1)
If I understand you correctly, I think this should solve your problems:)
You don't need a custom hood. Read more about reselect or similar.
If you don't want any libraries - just write your custom select function
const selectVal1 = ({ admin }: IRootReducer) => admin.val1;
and use it
const val1 = useSelector(selectVal1);
UPDATE
What about this?.
[JS]
const useAdminState = (key) => useSelect((state) => state.admin[key]);
[TS]
const useAdminState = (key: keyof IRootReducer['admin']) => useSelect(({ admin }: IRootReducer) => admin[key]);
There's a bunch of articles out there that show how Redux can be replaced with context and hooks (see this one from Kent Dodds, for instance). The basic idea is to make your global state available through a context instead of putting it inside a Redux store. But there's one big problem with that approach: components that subscribe to the context will be rerendered whenever any change happens to the context, regardless of whether or not your component cares about the part of the state that just changed. For functional components, React-redux solves this problem with the useSelector hook. So my question is: can a hook like useSelector be created that would grab a piece of the context instead of the Redux store, would have the same signature as useSelector, and, just like useSelector, would only cause rerenders to the component when the "selected" part of the context has changed?
(note: this discussion on the React Github page suggests that it can't be done)
No, it's not possible. Any time you put a new context value into a provider, all consumers will re-render, even if they only need part of that context value.
That's specifically one of the reasons why we gave up on using context to propagate state updates in React-Redux v6, and switched back to using direct store subscriptions in v7.
There's a community-written React RFC to add selectors to context, but no indication the React team will actually pursue implementing that RFC at all.
As markerikson answers, it is not possible, but you can work around it without using external dependencies and without falling back to doing manual subscriptions.
As a workaround, you can let the component re-render, but skip the VDOM reconciliation by memoizing the returned React element with useMemo.
function Section(props) {
const partOfState = selectPartOfState(useContext(StateContext))
// Memoize the returned node
return useMemo(() => {
return <div>{partOfState}</div>
}, [partOfState])
}
This is because internally, when React diffs 2 versions of virtual DOM nodes, if it encountered the exact same reference, it will skip reconciling that node entirely.
I created a toolkit for managing state using ContextAPI. It provides useSelector (with autocomplete) as well as useDispatch.
The library is available here:
https://www.npmjs.com/package/react-context-toolkit
https://github.com/bergkvist/react-context-toolkit
It uses:
use-context-selector to avoid unneccesary rerenders.
createSlice from #reduxjs/toolkit to make the state more modular and to avoid boilerplate.
I've created this small package, react-use-context-selector, and it just does the job.
I used the same approach as used in Redux's useSelector. It also comes with type declarations and the return type matches the selector function's return type making it suitable for using in TS project.
function MyComponent() {
// This component will re-render only when the `name` within the context object changes.
const name = useContextSelector(context, value => value.name);
return <div>{name}</div>;
}
Here is my take on this problem:
I used the function as child pattern with useMemo to create a generic selector component:
import React, {
useContext,
useReducer,
createContext,
Reducer,
useMemo,
FC,
Dispatch
} from "react";
export function createStore<TState>(
rootReducer: Reducer<TState, any>,
initialState: TState
) {
const store = createContext({
state: initialState,
dispatch: (() => {}) as Dispatch<any>
});
const StoreProvider: FC = ({ children }) => {
const [state, dispatch] = useReducer(rootReducer, initialState);
return (
<store.Provider value={{ state, dispatch }}>{children}</store.Provider>
);
};
const Connect: FC<{
selector: (value: TState) => any;
children: (args: { dispatch: Dispatch<any>; state: any }) => any;
}> = ({ children, selector }) => {
const { state, dispatch } = useContext(store);
const selected = selector(state);
return useMemo(() => children({ state: selected, dispatch }), [
selected,
dispatch,
children
]);
};
return { StoreProvider, Connect };
}
Counter component:
import React, { Dispatch } from "react";
interface CounterProps {
name: string;
count: number;
dispatch: Dispatch<any>;
}
export function Counter({ name, count, dispatch }: CounterProps) {
console.count("rendered Counter " + name);
return (
<div>
<h1>
Counter {name}: {count}
</h1>
<button onClick={() => dispatch("INCREMENT_" + name)}>+</button>
</div>
);
}
Usage:
import React, { Reducer } from "react";
import { Counter } from "./counter";
import { createStore } from "./create-store";
import "./styles.css";
const initial = { counterA: 0, counterB: 0 };
const counterReducer: Reducer<typeof initial, any> = (state, action) => {
switch (action) {
case "INCREMENT_A": {
return { ...state, counterA: state.counterA + 1 };
}
case "INCREMENT_B": {
return { ...state, counterB: state.counterB + 1 };
}
default: {
return state;
}
}
};
const { Connect, StoreProvider } = createStore(counterReducer, initial);
export default function App() {
return (
<StoreProvider>
<div className="App">
<Connect selector={(state) => state.counterA}>
{({ dispatch, state }) => (
<Counter name="A" dispatch={dispatch} count={state} />
)}
</Connect>
<Connect selector={(state) => state.counterB}>
{({ dispatch, state }) => (
<Counter name="B" dispatch={dispatch} count={state} />
)}
</Connect>
</div>
</StoreProvider>
);
}
Working example: CodePen
Solution with external store (Redux or Zustand like approach) with new hook useSyncExternalStore comes with React 18.
For React 18: Define createStore and useStore functions:
import React, { useCallback } from "react";
import { useSyncExternalStore } from "react";
const createStore = (initialState) => {
let state = initialState;
const getState = () => state;
const listeners = new Set();
const setState = (fn) => {
state = fn(state);
listeners.forEach((l) => l());
};
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
return { getState, setState, subscribe };
};
const useStore = (store, selector) =>
useSyncExternalStore(
store.subscribe,
useCallback(() => selector(store.getState()), [store, selector])
);
Now use it :
const store = createStore({ count: 0, text: "hello" });
const Counter = () => {
const count = useStore(store, (state) => state.count);
const inc = () => {
store.setState((prev) => ({ ...prev, count: prev.count + 1 }));
};
return (
<div>
{count} <button onClick={inc}>+1</button>
</div>
);
};
For React 17 and any React version that supports hooks:
Option 1: You may use the external library (maintained by React team)
use-sync-external-store/shim :
import { useSyncExternalStore } from "use-sync-external-store/shim";
Option 2: If you don't want to add new library and don't care about concurency problems:
const createStore = (initialState) => {
let state = initialState;
const getState = () => state;
const listeners = new Set();
const setState = (fn) => {
state = fn(state);
listeners.forEach((l) => l());
}
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
}
return {getState, setState, subscribe}
}
const useStore = (store, selector) => {
const [state, setState] = useState(() => selector(store.getState()));
useEffect(() => {
const callback = () => setState(selector(store.getState()));
const unsubscribe = store.subscribe(callback);
callback();
return unsubscribe;
}, [store, selector]);
return state;
}
Sources:
A conference talk from Daishi Kato from React Conf 2021
A blog post about same conference talk by Chetan Gawai
Simple approach to prevent additional renders with HoC and React.memo:
const withContextProps = (WrappedComponent) => {
const MemoizedComponent = React.memo(WrappedComponent);
return (props) => {
const state = useContext(myContext);
const mySelectedState = state.a.b.c;
return (
<MemoizedComponent
{...props}
mySelectedState={mySelectedState} // inject your state here
/>
);
};
};
withContextProps(MyComponent)
I have made a library, react-context-slices, which can solve what you are looking for. The idea is to break the store or state in slices of state, that is, smaller objects, and create a context for each one. That library which I told you does this, exposes a function createSlice which accepts a reducer, initial state, name of the slice, and a function for creating the actions. You create as slices as you want ('todos', 'counter', etc) and integrate them in a unique interface easily, exposing at the end two custom hooks, useValues and useActions, which can 'attack' all the slices (that is, in your client components you do not use useTodosValues but useValues). The key is that useValues accepts a name of the slice, so would be equivalent to the useSelector from redux. The library use immer as redux does. It's a very tiny library which the key point is how is used, which is explained in the readme file. I have also made a post about it. The library exposes only two functions, createSlice and composeProviders.
Let's say I have the following state:
state = {
products: {
50: {
sku: "000",
name: "A Product",
category: 123,
...
}
},
categories: {
123: {
name: "Some Category",
parentCategory: 100,
department: "Electronics"
}
},
filteredProducts: [50]
}
I want to be able to filter products based on categories. However, I need to filter based on multiple properties of categories. i.e. I might want to get all categories within the Electronics department or I might want to get a category with id 123 and all it's sub-categories.
This is a bit of a contrived example that closely matches what I'm trying to achieve but it's a bit easier to understand, so please bear with me. I'm aware that in this specific instance, I could probably use something like reselect, but assuming that I needed to do a category lookup for a products reducer, what would my options be?
You can use reselect as you mentioned, and make some selectors with parameter the re-use these selectors from categories in products to be as follow:
Make your category/selectors file as follow:
import { createSelector } from 'reselect';
const categoriesSelector = state => state.categories;
const selectCategoryById = id => {
return createSelector(
categoriesSelector,
categories => categories[id]
);
}
const selectCategoryByName = name => {
return createSelector(
categoriesSelector,
categories => categories.filter(c => c.name === name)
);
}
export default {
categoriesSelector,
selectCategoryById,
selectCategoryByName,
}
Meanwhile, in product/selector you can import both category and product selector files as follow:
import { createSelector } from 'reselect';
import { selectCategoryById } from './category/selectors';
const productsSelector = state => state.products;
const selectProductByCategoryId = id => {
return createSelector(
productsSelector,
selectCategoryById,
(products, categories) => products.filter(p.category.indexOf(id) > -1)
);
}
export default {
productsSelector,
selectProductByCategoryId,
}
And in product/reducer, you can import both selectors and return the new changed state based on category logic.