As a personal preference I wrap React props in namespaces. It helps me organize where different props come from.
With the useState hook I'm doing this.
function MyComponent() {
const [todoCount, setTodoCount] = useState(100);
const [doneCount, setDoneCount] = useState(0);
const myState = {
todoCount,
setTodoCount,
doneCount,
setDoneCount
};
return (
<>
<Text>Todo {myState.todoCount}</Text>
<Text>Done {myState.doneCount}</Text>
</>
);
}
Is there a more succinct syntax for state setup?
My failed attempt was
const myState = {
[todoCount, setTodoCount]: useState(100),
[doneCount, setDoneCount]: useState(0);
};
Sounds like the type of thing you could do as part of a custom hook e.g.
function useMappedState(defaultState = {}) {
const keys = Object.keys(defaultState);
return keys.reduce((map, k) => {
const fk = `set${k.charAt(0).toUpperCase()}${k.slice(1)}`;
const [state, setState] = useState(defaultState[k]);
map[k] = state;
map[fk] = setState;
return map;
}, {});
}
...
const state = useMappedState({
todoCount: 100,
doneCount: 0
});
console.log(state.todoCount) // 100
state.setTodoCount(5); // should set state of todoCount
In theory, this should give you what you want, but I've not tested so use with caution (e.g. I'm not even sure if hooks can be called can be called inside an iterator). - this works fine.
Although, what you are doing is really similar to what useReducer already does, might be worth some experimenting with that hook instead.
When you need to manage complex state, useReducer is the goto. It is a hook which accepts a reducer function in addition to initial state. The reducer is written by you to map certain "actions" to changes in state. You can "dispatch" an action to the reducer function to update state according to the rules you specify. useState itself internally calls useReducer.
/* action = { type: string, payload: any type } */
function reducer(state, { type, payload }) {
switch(type) {
case 'do-todo':
return { doneCount: state.doneCount + 1, todoCount: state.todoCount - 1 }
case 'undo-todo':
return { doneCount: state.doneCount - 1, todoCount: state.todoCount + 1 }
default:
return state
}
}
function App() {
const [ state, dispatch ] = useReducer(reducer, { todoCount: 100, doneCount: 0 })
return (
<>
<Text>Todo {state.todoCount}</Text>
<Text>Done {state.doneCount}</Text>
<Button onClick={() => dispatch({ type: 'do-todo' })}>Do Todo</Button>
</>
);
}
You can use multiple reducers to map to multiple namespaces.
Related
The useEffect doesn't fire on first render, but when I save the file (ctrl+s), the state updates and the results can be seen.
What I want to do is, when I'm in GameScreen, I tap on an ICON which takes me to WalletScreen, from there I can select some items/gifts (attachedGifts - in context) and after finalising I go back to previous screen i.e. GameScreen with gifts attached (attachedGifts!==null), now again when I tap ICON and go to WalletScreen it should show me the gifts that were attached so that I could un-attach them or update selection (this is being done in the useEffect below in WalletScreen), but the issue is, although my attachedGifts state is updating, the useEffect in WalletScreen does not fire immediately when navigated, when I hit ctrl+s to save the file, then I can see my selected/attached gifts in WalletScreen.
code:
const Main = () => {
return (
<GiftsProvider>
<Stack.Screen name='WalletScreen' component={WalletScreen} />
<Stack.Screen name='GameScreen' component={GameScreen} />
</GiftsProvider>
)
};
const GameScreen = () => {
const { attachedGifts } = useGifts(); //coming from context - GiftsProvider
console.log('attached gifts: ', attachedGifts);
return ...
};
const WalletScreen = () => {
const { attachedGifts } = useGifts();
useEffect(() => { // does not fire on initial render, after saving the file, then it works.
if (attachedGifts !== null) {
let selectedIndex = -1
let filteredArray = data.map(val => {
if (val.id === attachedGifts.id) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
setData(filteredArray);
}
}, [attachedGifts]);
const attachGiftsToContext = (obj) => {
dispatch(SET_GIFTS(obj));
showToast('Gifts attached successfully!');
navigation?.goBack(); // goes back to GameScreen
}
return (
// somewhere in between
<TouchableOpacity onPress={attachGiftsToContext}>ATTACH</TouchableOpacity>
)
};
context:
import React, { createContext, useContext, useMemo, useReducer } from 'react';
const GiftsReducer = (state: Object | null, action) => {
switch (action.type) {
case 'SET_GIFTS':
return action.payload;
default:
return state;
}
};
const GiftContext = createContext({});
export const GiftsProvider = ({ children }) => {
const initialGiftState: Object | null = null;
const [attachedGifts, dispatch] = useReducer(
GiftsReducer,
initialGiftState,
);
const memoedValue = useMemo(
() => ({
attachedGifts,
dispatch,
}),
[attachedGifts],
);
return (
<GiftContext.Provider value={memoedValue}>
{children}
</GiftContext.Provider>
);
};
export default function () {
return useContext(GiftContext);
}
Output of console.log in GameScreen:
attached gifts: Object {
"reciptId": "baNlCz6KFVABxYNHAHasd213Fu1",
"walletId": "KQCqSqC3cowZ987663QJboZ",
}
What could possibly be the reason behind this and how do I solve this?
EDIT
Added related code here: https://snack.expo.dev/uKfDPpNDr
From the docs
When you call useEffect in your component, this is effectively queuing
or scheduling an effect to maybe run, after the render is done.
After rendering finishes, useEffect will check the list of dependency
values against the values from the last render, and will call your
effect function if any one of them has changed.
You might want to take a different approach to this.
There is not much info, but I can try to suggest to put it into render, so it might look like this
const filterAttachedGifts = useMemo(() => ...your function from useEffect... , [attachedGitfs])
Some where in render you use "data" variable to render attached gifts, instead, put filterAttachedGifts function there.
Or run this function in component body and then render the result.
const filteredAttachedGifts = filterAttachedGifts()
It would run on first render and also would change on each attachedGifts change.
If this approach doesn't seems like something that you expected, please, provide more code and details
UPDATED
I assume that the problem is that your wallet receive attachedGifts on first render, and after it, useEffect check if that value was changed, and it doesn't, so it wouldn't run a function.
You can try to move your function from useEffect into external function and use that function in 2 places, in useEffect and in wallet state as a default value
feel free to pick up a better name instead of "getUpdatedArray"
const getUpdatedArray = () => {
const updatedArray = [...walletData];
if (attachedGifts !== null) {
let selectedIndex = -1
updatedArray = updatedArray.map((val: IWalletListDT) => {
if (val?.walletId === attachedGifts?.walletIds) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
setPurchaseDetailDialog(val);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
}
return updatedArray;
}
Then use it here
const [walletData, setWalletData] = useState(getUpdatedArray());
and in your useEffect
useEffect(() => {
setWalletData(getUpdatedArray());
}, [attachedGifts]);
That update should cover the data on first render. That might be not the best solution, but it might help you. Better solution require more code\time etc.
I am implementing a context that manages all the messages of a conversation.
To reduce the complexity of my algorithm, I have decided to use a Map "sectionsRef" for accessing some stuff in O(1).
This map, needs to be updated inside my reducer's logic, where I update the stateful data, in order to synchronize both.
export function MessagesProvider({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, initialState);
const sectionsRef = useMemo(() => new Map(), []);
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift));
};
const addMessage = (message) => addMessages([message]);
const deleteMessage = (messageId) => {
dispatch(actionCreators.deleteMessage(messageId));
};
const value = useMemo(() => ({
messages,
addMessages,
deleteMessage,
// eslint-disable-next-line react-hooks/exhaustive-deps
}), [messages]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
As you can see, I am using useMemo when initializing the Map in order to prevent re-initializations due to re-renders.
Is it correct to pass it as a payload to my reducer actions?
const addMessages = (messages, unshift = false) => {
dispatch(actionCreators.addMessages(messages, unshift, sectionsRef)); <---
};
To simplify my problem, imagine this is the real code:
//
// Reducer action
//
function reducerAction(state, messages, sectionsRef, title) {
state.push(...messages);
sectionsRef.set(title, state.length - 1);
}
//
// Context code
//
const state = [];
const firstMessagesSection = [{ id: 1 }];
const secondMessagesSection = [{ id: 1 }, { id: 2 }]
const sectionsRef = new Map();
reducerAction(state, firstMessagesSection, sectionsRef, "first section");
reducerAction(state, secondMessagesSection, sectionsRef, "second section");
console.log(state);
console.log(sectionsRef.get("second section"));
I am asking this because I have read that we shouldn't run side effects inside the reducers logic... so, if I need to synchronize that map with the state, what should I do instead?
Is it correct to pass it as a payload to my reducer actions?
No: reducers must be pure functions.
Redux describes reducers using a short list which I think is very useful:
Rules of Reducers​
We said earlier that reducers must always follow some special rules:
They should only calculate the new state value based on the state and action arguments
They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
They must not do any asynchronous logic or other "side effects"
The second and third items together describe pure functions, and the first one is just a Redux-specific convention.
In your example, you are violating two rules of pure functions:
mutating state with state.push(...messages) (rather than creating a new array and returning it), and
performing side-effects by modifying a variable in the outer scope: sectionsRef.set(title, state.length - 1)
Further, you seem to never use the Map (how is it accessed in your program?). It should be included in your context, and you can simply define it outside your component (its identity will never change so it won't cause a re-render).
Here's how you can refactor your code to achieve your goal:
Keep the reducer data pure:
// store.js
export function messagesReduer (messages, action) {
switch (action.type) {
case 'ADD': {
const {payload, unshift} = action;
return unshift ? [...payload, ...messages] : [...messages, ...payload];
}
case 'DELETE': {
const {payload} = action;
return messages.filter(m => m.id !== payload);
}
}
}
export const creators = {};
creators.add = (messages, unshift = false) => ({type: 'ADD', payload: messages, unshift});
creators.delete = (id) => ({type: 'DELETE', payload: id});
export const sections = new Map();
Update the Map at the same that you dispatch an action to the related state by combining those operations in a function:
// MessagesContext.jsx
import {
createContext,
useCallback,
useMemo,
useReducer,
} from 'react';
import {
creators,
messagesReduer,
sections,
} from './store';
export const MessagesContext = createContext();
export function MessagesProvider ({ children }) {
const [messages, dispatch] = useReducer(messagesReducer, []);
const addMessages = useCallback((title, messages, unshift = false) => {
dispatch(creators.add(messages, unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const addMessage = useCallback((title, message, unshift = false) => {
dispatch(creators.add([message], unshift));
sections.set(title, messages.length);
}, [creators.add, dispatch, messages]);
const deleteMessage = useCallback((id) => {
dispatch(creators.delete(id));
}, [creators.delete, dispatch]);
const value = useMemo(() => ({
addMessage,
addMessages,
deleteMessage,
messages,
sections,
}), [
addMessage,
addMessages,
deleteMessage,
messages,
sections,
]);
return (
<MessagesContext.Provider value={value}>
{children}
</MessagesContext.Provider>
);
}
Use the context:
// App.jsx
import {useContext} from 'react';
import {MessagesContext, MessagesProvider} from './MessagesContext';
function Messages () {
const {
// addMessage,
// addMessages,
// deleteMessage,
messages,
// sections,
} = useContext(MessagesContext);
return (
<ul>
{
messages.map(({id}, index) => (
<li key={id}>Message no. {index + 1}: ID {id}</li>
))
}
</ul>
);
}
export function App () {
return (
<MessagesProvider>
<Messages />
</MessagesProvider>
);
}
Additional notes:
Make sure your dependency lists (e.g. in useMemo, etc.) are exhaustive. Those lint warnings are there to help prevent you from making mistakes. In general, you should never need to suppress them.
I'm trying to display modal when no products have been selected by user before. I ended up having an infinite loop of useEffect() dependency. I'm not sure how to do it correctly in React.
import React, { useState, useEffect, useCallback } from 'react';
const MyComponent = ({ products }) => {
const [modals, setModals] = useState({});
const [currentModalName, setCurrentModalName] = useState('');
const setCurrentModal = useCallback(
(modalName, data = {}) => {
if (modalName) {
setModals({
...modals,
[modalName]: {
...modals[modalName],
...data
}
});
}
setCurrentModalName(modalName);
},
[modals]
);
useEffect(
() => {
if (!products.length) {
setCurrentModal('chooseProduct')
}
},
[products, setCurrentModal] // setCurrentModal causes infinite loop
);
return (
<div>...</div>
);
}
export default MyComponent;
I can just remove setCurrentModal from the dependencies, but I'm warned about it. If I add it, my React app freezes.
How can I organize my code to avoid freezing?
Why it loops?
The callback is always changing since it depends on the modals, which is always a different object even though it has the exact same properties as before, which always triggers the useEffect since it depends on the setCurrentModal callback value, which is always different since (() => {}) !== (() => {}).
Solution
Always use the functional update when the current state is needed to set the next state.
It'll prevent the need for the modals state as a dependency, which will limit the times when the callback is updated, fixing the infinite loop at the same time.
In addition to solving today's problem, functional update of the state is less prone to race-conditions, where multiple updates batched by React would overwrite each others.
const setCurrentModal = useCallback(
(modalName, data = {}) => {
if (!modalName) return; // precondition fail? early return.
// Happy path here!
// Note that I've used a different name to highlight that
// it's a different variable and to avoid shadowing the
// `modals` var from the outer scope.
setModals((currentModals) => ({ // use functional update.
...currentModals,
[modalName]: {
...currentModals[modalName],
...data
}
}));
setCurrentModalName(modalName);
},
// remove `modals` from the dependencies.
// setter functions are stable anyway, so it should remove any warning.
[setModals, setCurrentModalName]
);
useEffect(() => {
if (!products.length) {
setCurrentModal('chooseProduct')
}
},
[products, setCurrentModal]
);
Since the setCurrentModal callback is now stable (never ever changing), the useEffect will only be called when products value changes.
Missing dependencies warnings
The missing dependencies warnings come from the eslint-plugin-react-hooks, specifically, the react-hooks/exhaustive-deps rule. It's totally optional, but it helps keep the code clean and safe.
You could also choose to disable the warning just for this line:
const setCurrentModal = useCallback(
(modalName, data = {}) => {
// ...
setModals(/* ... */);
setCurrentModalName(modalName);
},
[] // eslint-disable-line react-hooks/exhaustive-deps
);
I think you can simplify it, without using useCallback.
(tested with Next.js and had no warnings, but if you still have some, you should use the answer of #Emile Bergeron)
import React, { useState, useEffect } from 'react'
const MyComponent = ({ products }) => {
const [modals, setModals] = useState({})
const [currentModalName, setCurrentModalName] = useState('')
const setCurrentModal = (name, data) => {
if (name) {
setModals(prev => {
return { ...prev, [name]: { ...prev[name], ...data }}
})
setCurrentModalName(name)
}
}
useEffect(() => {
if (!products || !products.length) {
const modalName = 'chooseProduct'
const data = { data: 'data' }
setCurrentModal(modalName, data)
}
}, [products])
const modalsJsx = modals ? Object.keys(modals).map((x, i) => {
return <li key={`modal-${i}`}>{x}</li>
}) : ''
const addModal = () => {
const name = 'test' + Math.floor(Math.random() * Math.floor(300))
setCurrentModal(name, { data: 'Hey' })
}
return (
<div>
<p>Current Modal : {currentModalName}</p>
<p>Modals : </p>
<ul>
{modalsJsx}
</ul>
<button onClick={addModal}>Test</button>
</div>
)
}
export default MyComponent
The function with useCallback to avoid warnings :
const setCurrentModal = useCallback((name, data = {}) => {
if (name) {
setModals(prev => {
return { ...prev, [name]: { ...prev[name], ...data }}
})
setCurrentModalName(name)
}
}, [setModals, setCurrentModalName])
I'm working on a project and created a context that is supposed to store my projects data. I included the context dispatch inside of a useEffect in a component which is supposed to pass the data object to the context but I am running into an issue where I am an infinite loop. I completely simplified the structure and I still can't figure out what my problem is. I pasted the code below. Does anyone know what I could be doing wrong?
// DataContext.js
import React, { useReducer } from "react";
export const DataContext = React.createContext();
const dataContextInitialState = { test: 1 };
const dataContextReducer = (state, action) => {
switch (action.type) {
case "update":
console.log(state);
return {
...state,
action.value,
};
default:
return dataContextInitialState;
}
};
export const DataContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(
dataContextReducer,
dataContextInitialState
);
return (
<DataContext.Provider
value={{
state,
dispatch,
}}
>
{children}
</DataContext.Provider>
);
};
// Component that is accessing context
.
.
.
useEffect(() => {
dataContext.dispatch({
type: "update",
});
}, [dataContext]);
.
.
.
I think this happens because the useEffect gets called during the first render, it executes dispatch that calls your reducer which returns a new object { dataContextInitialState }.
The new obect is passed down to your component, useEffect checks if the dataContext object is the same as in the previous render, but it is different because it's a new object so it re-executes the useEffect and you have the loop.
A possible solution
From my understanding with this piece of code
const dataContextInitialState = { test: 1 };
case "update":
return {
dataContextInitialState,
};
your state becomes this:
{
dataContextInitialState: {
test: 1
}
}
I guess that what you wanted was to have a state which is an object with a key names test, you can try modifying your code like this:
case "update":
return dataContextInitialState;
Problem
useState always triggers an update even when the data's values haven't changed.
Here's a working demo of the problem: demo
Background
I'm using the useState hook to update an object and I'm trying to get it to only update when the values in that object change. Because React uses the Object.is comparison algorithm to determine when it should update; objects with equivalent values still cause the component to re-render because they're different objects.
Ex. This component will always re-render even though the value of the payload stays as { foo: 'bar' }
const UseStateWithNewObject = () => {
const [payload, setPayload] = useState({});
useEffect(
() => {
setInterval(() => {
setPayload({ foo: 'bar' });
}, 500);
},
[setPayload]
);
renderCountNewObject += 1;
return <h3>A new object, even with the same values, will always cause a render: {renderCountNewObject}</h3>;
};
Question
Is there away that I can implement something like shouldComponentUpdate with hooks to tell react to only re-render my component when the data changes?
If I understand well, you are trying to only call setState whenever the new value for the state has changed, thus preventing unnecessary rerenders when it has NOT changed.
If that is the case you can take advantage of the callback form of useState
const [state, setState] = useState({});
setState(prevState => {
// here check for equality and return prevState if the same
// If the same
return prevState; // -> NO RERENDER !
// If different
return {...prevState, ...updatedValues}; // Rerender
});
Here is a custom hook (in TypeScript) that does that for you automatically. It uses isEqual from lodash. But feel free to replace it with whatever equality function you see fit.
import { isEqual } from 'lodash';
import { useState } from 'react';
const useMemoizedState = <T>(initialValue: T): [T, (val: T) => void] => {
const [state, _setState] = useState<T>(initialValue);
const setState = (newState: T) => {
_setState((prev) => {
if (!isEqual(newState, prev)) {
return newState;
} else {
return prev;
}
});
};
return [state, setState];
};
export default useMemoizedState;
Usage:
const [value, setValue] = useMemoizedState({ [...] });
I think we would need to see a better real life example of what you are tying to do, but from what you have shared I think the logic would need to move upstream to a point before the state gets set.
For example, you could manually compare the incoming values in a useEffect before you update state, because this is basically what you are asking if React can do for you.
There is a library use-deep-compare-effect https://github.com/kentcdodds/use-deep-compare-effect that may be of use to you in this case, taking care of a lot of the manual effort involved, but even then, this solution assumes the developer is going to manually decide (based on incoming props, etc) if the state should be updated.
So for example:
const obj = {foo: 'bar'}
const [state, setState] = useState(obj)
useEffect(() => {
// manually deep compare here before updating state
if(obj.foo === state.foo) return
setState(obj)
},[obj])
EDIT: Example using useRef if you don't use the value directly and don't need the component to update based on it:
const obj = {foo: 'bar'}
const [state, setState] = useState(obj)
const { current: payload } = useRef(obj)
useEffect(() => {
// always update the ref with the current value - won't affect renders
payload = obj
// Now manually deep compare here and only update the state if
//needed/you want a re render
if(obj.foo === state.foo) return
setState(obj)
},[obj])
Is there away that I can implement something like shouldComponentUpdate with hooks to tell react to only re-render my component when the data changes?
Commonly, for state change you compare with previous value before rendering with functional useState or a reference using useRef:
// functional useState
useEffect(() => {
setInterval(() => {
const curr = { foo: 'bar' };
setPayload(prev => (isEqual(prev, curr) ? prev : curr));
}, 500);
}, [setPayload]);
// with ref
const prev = useRef();
useEffect(() => {
setInterval(() => {
const curr = { foo: 'bar' };
if (!isEqual(prev.current, curr)) {
setPayload(curr);
}
}, 500);
}, [setPayload]);
useEffect(() => {
prev.current = payload;
}, [payload]);
For completeness, "re-render my component when the data changes?" may be referred to props too, so in this case, you should use React.memo.
If your function component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
The generic solution to this that does not involve adding logic to your effects, is to split your components into:
uncontrolled container with state that renders...
dumb controlled stateless component that has been memoized with React.memo
Your dumb component can be pure (as if it had shouldComponentUpdate implemented and your smart state handling component can be "dumb" and not worry about updating state to the same value.
Example:
Before
export default function Foo() {
const [state, setState] = useState({ foo: "1" })
const handler = useCallback(newValue => setState({ foo: newValue }))
return (
<div>
<SomeWidget onEvent={handler} />
Value: {{ state.foo }}
</div>
)
After
const FooChild = React.memo(({foo, handler}) => {
return (
<div>
<SomeWidget onEvent={handler} />
Value: {{ state.foo }}
</div>
)
})
export default function Foo() {
const [state, setState] = useState({ foo: "1" })
const handler = useCallback(newValue => setState({ foo: newValue }))
return <FooChild handler={handler} foo={state.foo} />
}
This gives you the separation of logic you are looking for.
You can use memoized components, they will re-render only on prop changes.
const comparatorFunc = (prev, next) => {
return prev.foo === next.foo
}
const MemoizedComponent = React.memo(({payload}) => {
return (<div>{JSON.stringify(payload)}</div>)
}, comparatorFunc);