I have a list of warehouses that I pull from an API call. I then render a list of components that render checkboxes for each warehouse. I keep the state of the checkbox in an object (using the useState hook). when I check/uncheck the checkbox, I update the object accordingly.
My task is to display a message above the checkbox when it is unchecked. I tried simply using the object, however, the component was not re-rendering when the object changed.
I found a solution to my problem by simply adding another useState hook (boolean value) that serves as a toggle. Since adding it, the component re-renders and my object's value is read and acted on appropriately.
My question is: why did I have to add the toggle to get React to re-render the component? Am I not updating my object in a manner that allows React to see the change in state? Can someone explain to me what is going on here?
I've created a sandbox to demonstrate the issue: https://codesandbox.io/s/intelligent-bhabha-lk61n
function App() {
const warehouses = [
{
warehouseId: "CHI"
},
{
warehouseId: "DAL"
},
{
warehouseId: "MIA"
}
];
const [warehouseStatus, setWarehouseStatus] = useState({});
const [toggle, setToggle] = useState(true);
useEffect(() => {
if (warehouses.length > 0) {
const warehouseStates = warehouses.reduce((acc, item) => {
acc[item.warehouseId] = true;
return acc;
}, {});
setWarehouseStatus(warehouseStates);
}
}, [warehouses.length]);
const handleChange = obj => {
const newState = warehouseStatus;
const { name, value } = obj;
newState[name] = value;
setWarehouseStatus(newState);
setToggle(!toggle);
};
return warehouses.map((wh, idx) => {
return (
<div key={idx}>
{!warehouseStatus[wh.warehouseId] && <span>This is whack</span>}
<MyCheckbox
initialState
id={wh.warehouseId}
onCheckChanged={handleChange}
label={wh.warehouseId}
/>
</div>
);
});
}
Thanks in advance.
You are mutating state (don't mutate state)
this:
const handleChange = obj => {
const newState = warehouseStatus;
const { name, value } = obj;
newState[name] = value;
setWarehouseStatus(newState);
};
should be:
const handleChange = ({name,value}) => {
setWarehouseStatus({...warehouseStatus,[name]:value});
};
See the problem?
const newState = warehouseStatus; <- this isn't "newState", it's a reference to the existing state
const { name, value } = obj;
newState[name] = value; <- and now you've gone and mutated the existing state
You then call setState with the same state reference (directly mutated). React says, "hey, that's the same reference to the state I previously had, I don't need to do anything".
Related
I have a ToDo component and an Item component in my react app. I noticed that when I trigger my onDelete function from my Item component, it only has the tdList state variable in the state it was in when I created the item component. Why does this happen and how can I fix this issue.
function ToDo() {
const [tdList, setTD] = useState([]);
const [item, setItem] = useState("");
const onDelete = (id) => {
// console.log(id);
console.log(tdList);
for(let i=0; i<tdList.length; i++){
if (tdList[i].props.id == id){
// setTD(tdList.splice(i, 1))
}
}
// setTD(tdList.splice())
};
const onHandleSubmit = (event) => {
event.preventDefault();
setTD([...tdList, (<Item id={itemsAdded} item={item} delete={onDelete} />)]);
setItem('');
// console.log(tdList);
itemsAdded++;
};
...more code...
Don't put React components into state. It breaks the natural order of how they're supposed to work and can make the control flow that's been written very difficult to understand. Instead, into state, put only the values needed to create React components from later - and when returning from the component, create the components from that state.
For your code, you could do something like:
const [lastIdUsed, setLastIdUsed] = useState(-1); // use this instead of reassigning a non-React itemsAdded variable
const [tdData, setTdData] = useState([]);
const onDelete = (id) => {
// use `.filter`, not `.splice` in React - don't mutate state
setTdData(tdData.filter(tdItem => tdItem.id !== id));
};
const onHandleSubmit = (event) => {
event.preventDefault();
setTdData([...tdData, { id: lastIdUsed + 1, item }]);
setItem('');
setLastIdUsed(lastIdUsed + 1);
};
const tds = tdData.map(
tdItem => <Item id={tdItem.id} item={tdItem.item} delete={onDelete} />
);
And then with the tds, return them or interpolate them into the JSX at the end.
Only create components right before you're going to return them.
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.
Trying to write a custom implementation of useState. Let's say only for a single value.
function useMyState(initVal){
const obj = {
value: initVal,
get stateValGet() {
return this.value
},
set stateValSet(val) {
this.value = val
}
};
const setVal = (val) => {
obj.stateValSet = val
}
return [obj.stateValGet, setVal]
}
Doesn't seem to work though, can anyone tell why?
Unable to crack this.
It returns this [, <function_setter>]
So if you try to run this setVal method, it does trigger the setter. But getter never gets called upon the updation.
useState's functionality can't really be polyfilled or substituted with your own custom implementation, because it not only stores state, but it also triggers a component re-render when the state setter is called. Triggering such a re-render is only possible with access to React internals, which the surface API available to us doesn't have access to.
useState can't be replaced with your own implementation unless that implementation also uses useState itself in order to get the component it's used in to re-render when the state setter is called.
You could create your own custom implementation outside of React, though, one which simulates a re-render by calling a function again when the state setter is called.
const render = () => {
console.log('rendering');
const [value, setValue] = useMyState(0);
document.querySelector('.root').textContent = value;
const button = document.querySelector('.root')
.appendChild(document.createElement('button'));
button.addEventListener('click', () => setValue(value + 1));
button.textContent = 'increment';
};
const useMyState = (() => {
let mounted = false;
let currentState;
return (initialValue) => {
if (!mounted) {
mounted = true;
currentState = initialValue;
}
return [
currentState,
(newState) => {
currentState = newState;
render();
}
];
};
})();
render();
<div class="root"></div>
Every state manager that wants to interact with React has to find a way to connect to React lifecycle, in order to be able to trigger re-renders on state change. useState hook internally uses useReducer:
https://github.com/facebook/react/blob/16.8.6/packages/react-dom/src/server/ReactPartialRendererHooks.js#L254
That's why I made this naive implementation of useState based on JavaScript Proxies and a useReducer dummy dispatch just to force a re-render when state changes.
It's naive, but that's what valtio is based on.
Consider that the power of proxies would make it possible to trigger re-renders by mutating state directly, that's what happens in valtio!
import { useReducer, useCallback, useMemo } from 'react';
export const useMyState = (_state) => {
// FORCE RERENDER
const [, rerender] = useReducer(() => ({}));
const forceUpdate = useCallback(() => rerender({}), []);
// INITIALIZE STATE AS A MEMOIZED PROXY
const { proxy, set } = useMemo(() => {
const target = {
state: _state,
};
// Place a trap on setter, to trigger a component rerender
const handler = {
set(target, prop, value) {
console.log('SETTING', target, prop, value);
target[prop] = value;
forceUpdate();
return true;
},
};
const proxy = new Proxy(target, handler);
const set = (d) => {
const value = typeof d === 'function' ? d(proxy.state) : d;
if (value !== proxy.state) proxy.state = value;
};
return { proxy, set };
}, []);
return [proxy.state, set];
};
Demo https://stackblitz.com/edit/react-33fpbk?file=src%2FApp.js
I have a React component that fetches data using the useEffect hook like so:
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
useEffect(() => {
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
}, [key, options])
return <p>{data}</p>;
}
This runs the hook every time key or options change. However, I'm also caching the data locally, and only want the effect to run when both key AND options change (since for each key/options combination the data will always be the same).
Is there a clean way to depend on the combination of key AND options rather than key OR options using React Hooks?
You can create this sort of logic with useRef(). Consider the following example and sandbox: https://codesandbox.io/s/react-hooks-useeffect-with-multiple-reqs-6ece5
const App = () => {
const [name, setName] = useState();
const [age, setAge] = useState();
const previousValues = useRef({ name, age });
useEffect(() => {
if (
previousValues.current.name !== name &&
previousValues.current.age !== age
) {
//your logic here
console.log(name + " " + age);
console.log(previousValues.current);
//then update the previousValues to be the current values
previousValues.current = { name, age };
}
});
return (
<div>
<input
placeholder="name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
placeholder="age"
value={age}
onChange={e => setAge(e.target.value)}
/>
</div>
);
};
Workflow:
We create a ref object for the two values we want to keep track of,
in this case its a name and age. The ref object is previousValues.
useEffect is defined but we do not provide it any dependencies.
Instead, we just have it execute whenever there is a state-change to
name or age.
Now inside useEffect we have conditional logic to check whether the
previous/initial values of both name and age are different than
their corresponding state-values. If they are then good we execute
our logic (console.log).
Lastly after executing the logic, update the ref object (previousValues) to the current values (state).
In order to run the effect when both values change, you need to make use of the previous values and compare them within the hook when either key or options change.
You can write a usePrevious hook and compare old and previous state as mentioned in this post:
How to compare oldValues and newValues on React Hooks useEffect?
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
const previous = usePrevious({key, options});
useEffect(() => {
if(previous.key !== key && previous.options !== options) {
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
}
}, [key, options])
return <p>{data}</p>;
}
All provided solutions are perfectly fine, However there are some more complex situation e.g., When useEffect function should be called ONLY when dependency A and B changed while it also depends on C's value.
So I suggest using sequence of useEffects and intermediate States to provide more space for future logics. Implementation of this approach for asked question would be:
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
const [needsUpdate, setNeedsUpdate] = useState(()=>({key:false, option:false}));
useEffect(()=>{
setNeedsUpdate((needsUpdate)=>({...needsUpdate, key:true}));
},[key])
useEffect(()=>{
setNeedsUpdate((needsUpdate)=>({...needsUpdate, options:true}));
},[options])
useEffect(() => {
if (needsUpdate.key && needsUpdate.options){
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
setNeedsUpdate(()=>({key:false, option:false}));
}
}, [needsUpdate, key, options])
return <p>{data}</p>;
}
In this way we can apply almost any logic on our useEffect dependencies, However it has own drawbacks which is few more rendering cycle.
You can create a new custom hook which calls the callback with an argument with index/names of dependencies
const useChangesEffect = (callback, dependencies, dependencyNames = null) => {
const prevValues = useRef(dependencies);
useEffect(() => {
const changes = [];
for (let i = 0; i < prevValues.current.length; i++) {
if (!shallowEqual(prevValues.current[i], dependencies[i])) {
changes.push(dependencyNames ? dependencyNames[i] : i);
}
}
callback(changes);
prevValues.current = dependencies;
}, dependencies);
};
useChangesEffect((changes) => {
if (changes.includes(0)) {
console.log('dep1 changed');
}
if (changes.includes(1)) {
console.log('dep2 changed');
}
}, [dep1, dep2]);
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);