Default value of state is not updated between renders - javascript

Consider following example. Im calling a fetching function in parent from child.
The request finishes, Im passing the data from async request to my Child as items prop. Im setting it as a default value for useState - React.useState(items);.
Expected behavior: request finishes, items is updated, Child gets new props, its re-rendered and the default value in useState is updated. So the a variable hold the proper object.
Actual behavior: the default value in useState is not updated between renders. Why?
const Child = ({ fn, items }) => {
const [a, b] = React.useState(items);
console.log(a, items); // a is null but items is an object
React.useEffect(() => {
fn();
}, []);
return JSON.stringify(a);
}
const App = () => {
const [stateOne, setStateOne] = React.useState(null);
const fn = () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => {
setStateOne(json);
});
}
console.log(stateOne)
return <Child fn={fn} items={stateOne} />;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>

The default value is intentionally only used when the component mounts. That's what's meant by "default". After that, the only way to change the state is by calling the state-setter function (b in your code).
It's rare that you need to copy a prop into state, so the likely fix is to just delete the state entirely, and use the prop.
const Child = ({ fn, items }) => {
React.useEffect(() => {
fn();
}, []);
return JSON.stringify(items);
}
If you do need to have state for some reason, first consider if you can move that state up to the parent and thus eliminate this issue. If for some reason you can't do that either, then you'll need to have the child implement logic which calls the state setter b when you want it to be called. For example:
const Child = ({ fn, items }) => {
const [a, b] = React.useState(items);
React.useEffect(() => {
b(items);
}, [items]);
React.useEffect(() => {
fn();
}, []);
return JSON.stringify(a);
}

You misunderstood what it means to have a "default value" in the case of React hooks. It's not so much a "default value" as an "initial value".
Consider the following code:
let n = Math.random();
const [myN, setMyN] = useState(n);
console.log(n) // This number will always be the same between renders
In your case, the initial value is null. It will never change unless if you call the setter for it.
If you're looking to pass down a prop, don't mix it into state.

Related

Functional component with React.memo() still rerenders

I have a button component that has a button inside that has a state passed to it isActive and a click function. When the button is clicked, the isActive flag will change and depending on that, the app will fetch some data. The button's parent component does not rerender. I have searched on how to force stop rerendering for a component and found that React.memo(YourComponent) must do the job but still does not work in my case. It also make sense to pass a check function for the memo function whether to rerender or not which I would set to false all the time but I cannot pass another argument to the function. Help.
button.tsx
interface Props {
isActive: boolean;
onClick: () => void;
}
const StatsButton: React.FC<Props> = ({ isActive, onClick }) => {
useEffect(() => {
console.log('RERENDER');
}, []);
return (
<S.Button onClick={onClick} isActive={isActive}>
{isActive ? 'Daily stats' : 'All time stats'}
</S.Button>
);
};
export default React.memo(StatsButton);
parent.tsx
const DashboardPage: React.FC = () => {
const {
fetchDailyData,
fetchAllTimeData,
} = useDashboard();
useEffect(() => {
fetchCountry();
fetchAllTimeData();
// eslint-disable-next-line
}, []);
const handleClick = useEventCallback(() => {
if (!statsButtonActive) {
fetchDailyData();
} else {
fetchAllTimeData();
}
setStatsButtonActive(!statsButtonActive);
});
return (
<S.Container>
<S.Header>
<StatsButton
onClick={handleClick}
isActive={statsButtonActive}
/>
</S.Header>
</S.Container>
)
}
fetch functions are using useCallback
export const useDashboard = (): Readonly<DashboardOperators> => {
const dispatch: any = useDispatch();
const fetchAllTimeData = useCallback(() => {
return dispatch(fetchAllTimeDataAction());
}, [dispatch]);
const fetchDailyData = useCallback(() => {
return dispatch(fetchDailyDataAction());
}, [dispatch]);
return {
fetchAllTimeData,
fetchDailyData,
} as const;
};
You haven't posted all of parent.tsx, but I assume that handleClick is created within the body of the parent component. Because the identity of the function will be different on each rendering of the parent, that causes useMemo to see the props as having changed, so it will be re-rendered.
Depending on if what's referenced in that function is static, you may be able to use useCallback to pass the same function reference to the component on each render.
Note that there is an RFC for something even better than useCallback; if useCallback doesn't work for you look at how useEvent is defined for an idea of how to make a better static function reference. It looks like that was even published as a new use-event-callback package.
Update:
It sounds like useCallback won't work for you, presumably because the referenced variables used by the callback change on each render, causing useCallback to return different values, thus making the prop different and busting the cache used by useMemo. Try that useEventCallback approach. Just to illustrate how it all works, here's a naive implementation.
function useEventCallback(fn) {
const realFn = useRef(fn);
useEffect(() => {
realFn.current = fn;
}, [fn]);
return useMemo((...args) => {
realFn.current(...args)
}, []);
}
This useEventCallback always returns the same memoized function, so you'll pass the same value to your props and not cause a re-render. However, when the function is called it calls the version of the function passed into useEventCallback instead. You'd use it like this in your parent component:
const handleClick = useEventCallback(() => {
if (!statsButtonActive) {
fetchDailyData();
} else {
fetchAllTimeData();
}
setStatsButtonActive(!statsButtonActive);
});

How to make React functional component recreate callback function and read updated props

I have a very simple functional component in React. When this component is rendered by the parent component, initially myList is an empty array, and then eventually when it finishes loading, it is a list with a bunch of items.
The problem is, the value of myList inside onSearchHandler never gets updated, it's always [].
const MyComponent = ({ myList }) => {
const [filteredList, setFilteredList] = useState(myList);
console.log(myList); // <<< This outputs [], and later [{}, {}, {}] which is expected.
useEffect(() => {
setFilteredList(myList);
}, [myList]);
const onSearchHandler = (searchText) => {
console.log(myList); /// <<< When this function is called, this always outputs []
const filteredItems = myList.filter(item =>
item.name.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredList(filteredItems);
};
return <AnotherComponent items={filteredList} onSearch={onSearchHandler} />
};
Is there a way to force onSearchHandler to re-evaluate the value of myList? What would be the recommended approach for this sort of operation?
It sounds like AnotherComponent does not take into consideration the changed prop - this should be considered to be a bug in AnotherComponent. Ideally, you'd fix it so that the changed prop gets used properly. Eg, just for an example, maybe it's doing
const [searchHandler, setSearchHandler] = useState(props.onSearch);
and failing to observe prop changes as it should. Or, for another random example, this could happen if the listener prop gets passed to an addEventListener when the component mounts but again doesn't get checked for changes and removed/reattached.
If you can't fix AnotherComponent, you can use a ref for myList in addition to the prop:
const MyComponent = ({ myList }) => {
const myListRef = useRef(myList);
useEffect(() => {
myListRef.current = myList;
setFilteredList(myList);
}, [myList]);
const [filteredList, setFilteredList] = useState(myList);
const onSearchHandler = (searchText) => {
const filteredItems = myListRef.current.filter(item =>
item.name.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredList(filteredItems);
};
It's ugly, but it might be your only option here.

react: functional component that ignores prop changes until callback is triggered

I have a controlled component that I want to use to trigger an optimistic update, and, while the update is being performed, it should use it's internal state and ignore all changes to it's main prop, and once a callback is executed then it can again use the provided prop as values.
I tried to encode this logic on a hook using useMemo, useRef, and different setStates but without success. I think it is because the used ref does not survive long enough (the component is re-rendered like 3 times between the update trigger and the callback execution) and at some point it gets re-created with the new value coming from props.
Here is the hook that I done so far:
function useOptimistic(incoming) {
const [state, setState] = useState('INITIAL')
const ref = useRef(incoming)
const value = useMemo(() => ref.current, [state])
const setUpdating = (value) => {
setState('UPDATING')
ref.current = value
}
const setUpdated = () => setState('UPDATED')
return { state: value, setUpdating, setUpdated }
}
The reason is because I am using graphql and a toggle component. When I click on the toggle component I trigger the graphql request and you can see the change with the local state, but while the query is being executed the component is re-rendered 3 or 4 times, and the toggle shows the state from the "old-data", when the request completes then it comes back to the correct state. What I want is to avoid this intermediary weird changes.
I think that this should do it:
const initialState = { updating: false }
function useOptimistic(incoming) {
const [innerState, setInnerState] = useState(initialState)
const setUpdating = (value) => setInnerState({ updating: true, value })
const setUpdated = () => setInnerState(initialState)
const state = innerState.updating ? innerState.value : incoming
return { state, setUpdating, setUpdated }
}
Live Example (based on T.J. Crowder's answer), just to show that it's not really necessary to have stable functions for something like this ;-).
const { useState, useRef } = React;
const initialState = { updating: false }
function useOptimistic(incoming) {
const [innerState, setInnerState] = useState(initialState)
const setUpdating = (value) => setInnerState({ updating: true, value })
const setUpdated = () => setInnerState(initialState)
const state = innerState.updating ? innerState.value : incoming
return { state, setUpdating, setUpdated }
}
function Example() {
const [value, setValue] = useState(1);
const {state, setUpdating, setUpdated} = useOptimistic(value);
const triggerUpdate = () => {
const newValue = value + 1;
setUpdating(newValue);
setTimeout(() => {
setValue(newValue);
setUpdated(); // <=== Note that despite the fact that this function
// is not stable, this still works perfectly fine ;-)
}, 1000);
};
return (
<div>
<div>value: {value}</div>
<div>state: {state}</div>
<input type="button" onClick={triggerUpdate} value="Trigger Update" />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>
A ref will only be created once, when the component using the hook is first mounted, so you were right to try to store the pending state in a ref (although it could also be stored in state). You'd also probably be best off putting the state setters in a ref as well, since making them stable (like useState does) is useful to consumers of the hook, so I'll use the ref for the pending value as well.
The main problem with your hook, I think, is that it always uses the value from ref.current, but it only sets ref.current to incoming when the component using the hook is first mounted. So it ignores subsequent changes to incoming (whether or not it's in the updating state).
The change I'd make (other than using a boolean rather than strings for the flag) is to use your "updating" flag to choose what to return as state, returning the then-current incoming when not updating, or the optimistic pending value if updating. See comments:
function useOptimistic(incoming) {
// A flag for whether we're in the "udating" state
const [updatingFlag, setUpdatingFlag] = useState(false)
// Our instance data
const instance = useRef(null);
if (!instance.current) {
// Only happens on first call, this object is created once
instance.current = {
// The pending value
pendingValue: null,
// Set the pending value and go into the updating state
setUpdating: (value) => {
instance.current.pendingValue = value;
setUpdatingFlag(true);
},
// Exit the updating state
setUpdated: () => {
setUpdatingFlag(false);
instance.current.pendingValue = null;
},
};
}
// The state value we'll return: the input prop, or the one from our
// instance data
const state = updatingFlag ? instance.current.pendingValue : incoming;
// Grab our setters and return them with the state
const { setUpdating, setUpdated } = instance.current;
return { state, setUpdating, setUpdated };
}
Live Example:
const { useState, useRef } = React;
function useOptimistic(incoming) {
// A flag for whether we're in the "udating" state
const [updatingFlag, setUpdatingFlag] = useState(false)
// Our instance data
const instance = useRef(null);
if (!instance.current) {
// Only happens on first call, this object is created once
instance.current = {
// The pending value
pendingValue: null,
// Set the pending value and go into the updating state
setUpdating: (value) => {
instance.current.pendingValue = value;
setUpdatingFlag(true);
},
// Exit the updating state
setUpdated: () => {
setUpdatingFlag(false);
instance.current.pendingValue = null;
},
};
}
// The state value we'll return: the input prop, or the one from our
// instance data
const state = updatingFlag ? instance.current.pendingValue : incoming;
// Grab our setters and return them with the state
const { setUpdating, setUpdated } = instance.current;
return { state, setUpdating, setUpdated };
}
function Example() {
const [value, setValue] = useState(1);
const {state, setUpdating, setUpdated} = useOptimistic(value);
const triggerUpdate = () => {
const newValue = value + 1;
setUpdating(newValue);
setTimeout(() => {
setValue(newValue);
setUpdated(); // <=== Note that the stability of the setters is what
// lets me use this here, it wouldn't be correct if
// the setters weren't stable
}, 1000);
};
return (
<div>
<div>value: {value}</div>
<div>state: {state}</div>
<input type="button" onClick={triggerUpdate} value="Trigger Update" />
</div>
);
}
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

Why is Gatsby arrow function not able to see a value passed in props where value is derived from promise?

Can someone please explain why the value of key within the arrow function is undefined:
// in parent component
const Parent = () => {
const [key, setKey] = useState<string>();
// this contains an expensive function we only wish to execute once on first load
useEffect(() => {
// has some promise that will call within a `then()`
setKey(someVal);
}, []};
// within render
< MyComponent key={key}/>
}
// within child component
interface Props {
key: string;
}
const MyComponent = ({key}: Props) => {
// this works, I can see the correct value `someVal`
console.log("value when rendered: " + key);
const callback = () => {
// this isn't working, key is undefined
console.log("value within callback: " + key);
}
// within render, when something calls callback(), key is undefined, why?
}
I can see that key has a value when the render is called, but key is undefined. I've tried using let callback = instead of const, but no luck. How do I access key please?
In React, key is a reserved prop name.
[...] attempting to access this.props.key from a component (i.e., the render function or propTypes) is not defined
https://reactjs.org/warnings/special-props.html
Which is probably the reason why it doesn't work in subsequent renders — I'm surprised that it worked in the first render at all!
This works fine:
// https://codepen.io/d4rek/pen/PoZRWQw
import { nanoid } from 'https://cdn.jsdelivr.net/npm/nanoid/nanoid.js'
const Child = ({ id }) => {
console.log(`within component: ${id}`)
const cb = () => console.log(`in callback: ${id}`)
return <button onClick={cb}>Click me</button>
}
const Parent = () => {
const [id, setId] = React.useState(null)
React.useEffect(() => {
setId(nanoid(6))
}, [])
return (<Child id={id} />)
}
ReactDOM.render(<Parent />, document.body)
import React, { useCallback } from 'react';
const callback = useCallback(() => {
// this isn't working, key is undefined
console.log("value within callback: " + key);
}, [key]);
The reason why yours is not working: props are bound to this but the callback as you defined it has its own scope and hence has its own this. So you need to provide the value to that scope. You can do it by using local state of the component. Since there are some nice hooks in React to make that easy you should use them to memorize the callback:
React.useCallback(() =>{console.log(key);}, [key]);
Note the dependency array that updates the callback when key changes. Here the scoping is fine.

Access the latest state from external function React JS

I have a simple React component (Hooks):
// inside React component
import { someExternalFunction } from "functions"
const [value, setValue] = useState(0)
const handleChange = () => {
someExternalFunction(value, setValue)
}
// outside of React component
const someExternalFunction = (value, setValue) => {
console.log(value) // 0
// testing "set" method
setValue(100) // working
// "set" is async, so lets wait
// testing "get" method
setTimeout(() => console.log(value), 5000) // 0
// not working
}
PROBLEM: "value"/state is always the same, it is captured at the moment when state is passed to a function.
QUESTION: How to access the latest state in external function?
CLARIFICATION: Of course it is not working with the "value"/state, I just tried to illustrate the problem that I'm facing with preferred method for providing the latest state to the function (trouh the direct reference) .
Value is primitive type, not reference type, it was pass into function as a copied value, of cause it is never changed. And setTimeout always access a copied value.
As long as you don't trigger "handleChange" again, the function won't use the new value. Your state is a number so it will not pass a reference but rather the value itself (in this case 0). When you call setValue, the state is updated but your timeOut callback still uses the stale value that was passed when you triggered "handleChange".
If you want to react to a state change a useEffect hook would be better.
You can also simply put a console.log(value) below the line where you define the state, to check if it changes.
// Update:
const [value, setValue] = React.useState(0);
const handleChange = () => {
setValue(100);
}
React.useEffect(() => console.log(value), [value]);
Or if you want to move that logic to a reusable hook:
// useExternalFunction.js
export useExternalFunction = () => {
const [value, setValue] = React.useState(0);
const handleChange = () => {
setValue(100);
}
React.useEffect(() => console.log(value), [value]);
return {value, handleChange};
}
// component
const {value, handleChange} = useExternalFunction();
return <button onClick={handleChange}>{value}</button>;

Categories