React : parent props changes, but children (with map) update one step behind - javascript

I pass a prop data which contain a field worktypeData witch has a field that changes.
When I console log I can see data updated, but the console log in the map is one step behind late.
I tried with a useEffect that set a new state with **data **in the dependencies, but same result.
Tried this too, but same result.
// this is the type of the field in OfficeOccupancyCardData that I want to update
interface WorkTypeData {
selected: WorkType;
onClick: (worktype: WorkType) => void;
buttonLink: string;
}
interface DashboardNextDaysProps {
data: OfficeOccupancyCardData[];
}
const DashboardNextDays: React.FunctionComponent<DashboardNextDaysProps> = ({ data }) => {
// console log here will show data with the new value
return (
<HorizontalGridSlide className="DashboardNextDays" colSize={286} gap={16} pageSlide>
{data.length > 0 &&
data.map((day, i) => {
// console log here will show data with the value not updated
return <OfficeOccupancyCard key={generateKey(i)} data={day} />;
})}
</HorizontalGridSlide>
);
};
EDIT: I found a solution, if someone can explain to me why this works.
In the parent of DashboardNextDays I have a useEffect to set the new data :
useEffect(() => {
setNextDaysData((prev) => {
const newNextDaysData = prev;
if (newNextDaysData && nextDayWorktypeSelected)
newNextDaysData[nextDayWorktypeSelected.dayIndex] = {
...newNextDaysData?.[nextDayWorktypeSelected.dayIndex],
worktypeData: {
...newNextDaysData?.[nextDayWorktypeSelected.dayIndex].worktypeData,
selected: nextDayWorktypeSelected.worktype,
},
};
return newNextDaysData ? newNextDaysData : [];
});
}, [nextDayWorktypeSelected]);
And I just changed
return newNextDaysData ? newNextDaysData : [];
to
return newNextDaysData ? [...newNextDaysData] : [];

To see the state changes immediately, you need to use useEffect and add newData in the dependency change.

Since setState is asynchronous you can not check state updates in console.log synchronously.

useEffect is the hook that manages side-effects in functional components:
useEffect(callback, dependencies). The callback argument is a function to put the side-effect logic in place. Dependencies is a list of your side effect's dependencies, whether they are props or state values.
import { useEffect } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState();
useEffect(() => {
// Side-effect uses `prop` and `state`
}, [prop, state]);
return <div>....</div>;
}

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);
});

setState in useEffect not working in conjunction with localStorage

This seems unordinary. I'm attempting to simply setState on mount, but it's just going blank on me.
const [info, setState] = useState({
user_1: '',
user_2: '',
});
useEffect(() => {
var personal_1 = window.localStorage.getItem('personal_1');
var personal_2 = window.localStorage.getItem('personal_2');
console.log(personal_1);
if (personal_1 !== null && personal_2 !== null) {
console.log('Make it easy')
setState({
...info,
user_1: personal_1,
user_2: personal_2
})
}
}, [])
useEffect(() => {
console.log('User personal_1 was changed to', info.user_1);
}, [info.user_1])
When I log the effects of this component mounting,
it shows
personal_1
Make it easy
User personal_1 was changed to
For some reason instead of setting the state on mount to the value from local storage, it just goes blank. I thought it was probably being set back to being blank from somewhere else in the component, but the info.personal_1 hook only gets called once. Does anyone know why it might potentially be setting the local storage item, but then going blank when the storage item is a normal string?
This is happening because you are not successfully setting the local storage values. Hence, the value of those items will always be blank
You can straight out use the custom React hook (useLocalStorage) I am providing below to get and set items from your localStorage into your web-app
Hook
import { useCallback, useState, useEffect } from "react"
// To use local storage to store, update info
export function useLocalStorage(key, defaultValue) {
return useStorage(key, defaultValue, window.localStorage)
}
// Common code being used irresp of storage type -> local or session
function useStorage(key, defaultValue, storageObject) {
// Set the "value" (in this hook's state, and return it to the component's state as well) to be the value at passed "key" in storage.
// Else, we set it to the defaultValue (which can be a value or a func returning a val)
const [value, setValue] = useState(() => {
const jsonValue = storageObject.getItem(key)
if (jsonValue != null) return JSON.parse(jsonValue)
if (typeof defaultValue === "function") {
return defaultValue()
} else {
return defaultValue
}
})
// To set a value in the storage using key-val pair
useEffect(() => {
if (value === undefined) return storageObject.removeItem(key)
storageObject.setItem(key, JSON.stringify(value))
}, [key, value, storageObject])
// To remove value at given key
const remove = useCallback(() => {
setValue(undefined)
}, [])
return [value, setValue, remove]
}
Using the hook in a component :
import { useLocalStorage } from "./useStorage"
export default function StorageComponent() {
const [age, setAge, removeAge] = useLocalStorage("age", 26)
return (
<div>
<div>
{age}
</div>
<button onClick={() => setAge(40)}>Set Age</button>
<button onClick={removeAge}>Remove Age</button>
</div>
)
}
in second useEffect dependency just add info like this:
useEffect(() => {
if(info.user_1){
console.log('User personal_1 was
changedto',info.user_1);
}
}, [info])

Default value of state is not updated between renders

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.

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.

useSelector not updating when store has changed in Reducer. ReactJS Redux

I am changing the state in reducer. On debug I checked that the state was really changed. But the component is not updating.
Component:
function Cliente(props) {
const dispatch = useDispatch()
const cliente = useSelector(({ erpCliente }) => erpCliente.cliente)
const { form, handleChange, setForm } = useForm(null)
...
function searchCepChangeFields() {
//This call the action and change the store on reducer
dispatch(Actions.searchCep(form.Cep))
.then(() => {
// This function is only taking values ​​from the old state.
// The useSelector hook is not updating with store
setForm(form => _.setIn({...form}, 'Endereco', cliente.data.Endereco))
setForm(form => _.setIn({...form}, 'Uf', cliente.data.Uf))
setForm(form => _.setIn({...form}, 'Cidade', cliente.data.Cidade))
setForm(form => _.setIn({...form}, 'Bairro', cliente.data.Bairro))
})
}
Reducer:
case Actions.SEARCH_CEP:
{
return {
...state,
data: {
...state.data,
Endereco: action.payload.logradouro,
Bairro: action.payload.bairro,
UF: action.payload.uf,
Cidade: action.payload.cidade
}
};
}
NOTE: you better start using redux-toolkit to prevent references
in you code its a better and almost a must way for using redux
the problem your facing is very common when handling with objects,
the props do not change because you're changing an object property but the object itself does not change from the react side.
even when you're giving it a whole new object
react doesn't see the property object change because the reference stays the same.
you need to create a new reference like this:
Object.assign(state.data,data);
return {
...state,
data: {
...state.data,
Endereco: action.payload.logradouro,
Bairro: action.payload.bairro,
UF: action.payload.uf,
Cidade: action.payload.cidade
}
}
to add more you can learn about the Immer library that solves this
problem.
It's not necessary to
Object.assign(state.data, data);
always when changing data of arrays or objects
return(
object: {...state.object, a: 1, b: 2},
array: [...state.array, 1, 2, 3]
)
this 3 dots (...) ensure that you create a new object. On redux you have to always create a new object, not just update the state. This way redux won't verify that your data has changed.
When having nesting objects or arrays, is the same thing
Just have attention to:
initialState = {
object: {
...object,
anotherObject:{
...anotherObject,
a: 1,
b: 2
}
}
}
Somehow, the Object.assgin is not recognize
Update with ES6 syntax.
updatedConnectors = state.connectors
This will create a reference to the current state. In ES6, that introduce the ... to make new reference.
updatedConnectors = { ...state.connectors }
.....
return {
...state,
connectors: updatedConnectors
};
use this to extract and copy new reference. That will trigger state change too
Update Sep/27/20
I've wrote some utils function to handle this, Let try this
//Utils
export const getStateSection = ({ state, sectionName }) => {
const updatedState = { ...state }
const updatedSection = updatedState[sectionName]
return updatedSection
}
export const mergeUpdatedSection = ({ state, sectionName, updatedValue }) => {
const updatedState = { ...state }
updatedState[sectionName] = updatedValue
return updatedState
}
Then In any reducer, It should use like this:
//reducer
const initState = {
scheduleDetail: null,
timeSlots: null,
planDetail: {
timeSlots: [],
modifedTimeSlots: [],
id: 0
}
}
.....
const handlePickTimeSlot = (state, action) => {
let planDetail = getStateSection({ state, sectionName: 'planDetail' })
// do stuff update section
return mergeUpdatedSection({ state, sectionName: 'planDetail', updatedValue: planDetail })
}
Since the edit queue for elab BA is full.
The accepted answer here is what he meant by data being there
case MYCASE:
let newDataObject = Object.assign(state.data, {...action.payload});
// or
// let newDataObject = Object.assign(state.data, {key: 'value', 'key2': 'value2' ...otherPropertiesObject);
return {
...state,
...newDataObject
}
There is an interesting edge case that can happen when modifying the file where you create your Store.
If the file where you have your redux store Provider component (usually App.tsx) does not get reloaded by React's hot module reloader (HMR) but the redux store file gets modified and therefore reloaded by HMR, a new store is created and the store Provider in your App.tsx can actually end up passing an old instance of your redux store to useSelector.
I have left the following comment in my setup-store.ts file:
/**
* Note! When making changes to this file in development, perform a hard refresh. This is because
* when changes to this file are made, the react hot module reloading will re-run this file, as
* expected. But, this causes the store object to be re-initialized. HMR only reloads files that
* have changed, so the Store Provider in App.tsx *will not* be reloaded. That means useSelector
* values will be querying THE WRONG STORE.
*/
It is not a problem, you should understand how the React is working. It is expected behavior
In your case you are invoking
dispatch(Actions.searchCep(form.Cep))
.then(() => {...some logic...}
But all of this work in ONE render, and changed store you will get only in the NEXT render. And in then you are getting props from the first render, not from the next with updated values. Look for example below:
import React from 'react';
import { useSelector, useDispatch } from "react-redux";
import { selectCount, incrementAsync } from "./redux";
import "./styles.css";
export default function App() {
const value = useSelector(selectCount);
const dispatch = useDispatch();
const incrementThen = () => {
console.log("value before dispatch", value);
dispatch(incrementAsync(1)).then(() =>
console.log("value inside then", value)
);
};
console.log("render: value", value);
return (
<div className="App">
<p>{value}</p>
<button onClick={incrementThen}>incrementThen</button>
</div>
);
}
And output
value before dispatch 9
render: value 10
value inside then 9

Categories