For some reason, my card won't re-render when I make a call to the API.
when the information updates in the backend it doesn't display in the front unless I refresh the app.
I'm passing down info as a prop and grabbing the company name, address, and phone.
const CpoCard = ({ info, navigation }) => {
const [companyName, setCompanyName] = useState(info.companies[0].companyName);
const [address, setAddress] = useState(info.companies[0].city);
const [phoneNumber, setPhoneNumber] = useState(info.companies[0].phoneNumber);
const [employeeID, setEmployeeID] = useState(info.userId.employeeId);
const { setSelectedEmployee } = useCompany();
const handleEditPress = () => {
setSelectedEmployee(info);
navigation.navigate('DeleteEmployee');
//deleteEmployee
};
const handlePress = () => {
console.log(info);
navigation.navigate('CpoInfo', { info: info });
};
return (
<View style={styles.outerContainer}>
{
<View style={styles.innerContainer}>
<View style={styles.mainContent}>
<Image
style={styles.employeeImg}
source={require('~assets/pngs/avatar.png')}
/>
<View style={styles.companyInfoContainer}>
<TouchableOpacity onPress={handlePress}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>
{companyName !== null ? `${companyName} ` : 'info'}
</Text>
<Text>{`Phone Number: ${phoneNumber}`}</Text>
<Text>{`address: ${address}`}</Text>
</TouchableOpacity>
</View>
</View>
</View>
}
</View>
);
};
Whenever the prop info is being updated by the parent component, the CpoCard will re-render. You could simply test it by including console.log('rerendered) in the component.
Based on your wording in your question it seems the issue isint the re-render but more likely your structure of a CpoCard component.
In React, whenever the component first loads, it creates its states to whatever initial value you set in useState. Later down the line the state will not mutate nor charge unless the setter function is called. That triggers the component to re-render. You can NOT set the state value otherwise.
In your case, in the CpoCard component, the prop info is ONLY used to set the initial state value via useState(info.companies[0].companyName). So the value is being set on the first load of a component. But later on, regardless how info prop changes - the state will NOT change regardless of how many times the component re-renders.
Simple example:
At CpoCard load <CpoCard info={'initial value'} /> -> component CpoCard creates a state via useState and stores its value as passed const [info, setinfo] = useState(props.info) -> component renders.
Later down the line info prop changes <CpoCard info={'second value'} /> -> component CpoCard re-renders info from const [info, setinfo] = useState(props.info) remains the initial value 'initial value' since you can not change the state value like so. The initial value will remain for the entire time, regardless of how many re-renders, unless called the setter setInfo and changes.
So in your case your structure is the issue.
I am unsure why you even store each individual prop into its own state - it seems like a flaw in a structure by itself. If you are getting passed values - use those. If for some reason you actually require to use a state for each value then there is also a work around - useEffect
useEffect hook allows you to listen to changes and react based on that.
So you should call each setter in a useEffect that is listening to your info prop change:
useEffect(() => {
setCompanyName(info.companies[0].companyName)
setAddress(info.companies[0].city)
setPhoneNumber(info.companies[0].phoneNumber)
setEmployeeID(info.userId.employeeId)
}, [info])
With this useEffect what happens is the component loads the initial value, stores it in state and renders. Whenever the prop.info changes the component re-renders, the useEffect dependency array (the [] in the end) checks that prop.info changes hence it executes the function (the code inside the useEffect hook) that sets the states based on new info prop and that causes a component to re-render once again displaying the new data.
Related
I am doing a project where i have a toast function which implements toast there i call the function of fetching data from api and updating my state so that whenever i click the update feed button fetching data from api function called, updation of state and toast of success appears. Now the question is i have a component of categories post displays seperate category post inside of all post component which has the function to display toast, how could i pass the updated state,fetching data from api function from child component that is category post component to parent component that is all post component to implement toast for category component.
If I understand your question correctly -- at a high level, you're trying to figure out how to update a state variable of a parent component from within a child component. Easiest way would be with the useState hook, and then by passing the setState function to the child component.
const ParentComponent = () => {
const [state, setState] = useState([])
useEffect(() => {
// logic that will be executed every time the state variable changes
}, [state])
return <ChildComponent setState={setState} />
}
const ChildComponent = ({setState}) => {
const handleClick = () => {
setState((currentState) => currentState.concat(1))
}
return <Button onClick={handleClick} />
}
Edit: To answer your question from the comment -- a few things to point out here:
You can pass a value to useState which will be the starting value of the variable. In our example, it's an empty array
setState has access to the current state, so you can push a value to an array with this syntax: setState((previousState) => previousState.concat(val))
useEffect is a hook which is invoked whenever there's a change in the value of the dependency (or dependencies) passed in the second argument. So by including state in its dependency array, we can execute whatever logic we want every time the value of the state variable changes
I would also recommend looking into useMemo. It similarly allows you to have aspects of your component logic that are re-executed only when values of certain variables change. For example:
const ParentComponent = () => {
const [state, setState] = useState([])
useEffect(() => {
// logic that will be executed every time the state variable changes
}, [state])
const renderCards = useMemo(() => {
return state.map(val => <SomeOtherComponent val={val}/>)
}, [state])
return (
<div>
{renderCards}
<ChildComponent setState={setState} />
</div>
)
}
By wrapping the function inside renderCards in the useMemo hook, the evaluated result is "memoized". And so it won't be executed on every render, unless the variable in the dependency array changes.
Passing down setState to a child component in order to trigger a re-render in the parent component is straightforward when it's an immediate child. If the child component is nested deeper, or there are multiple components that need to react to a change in a variable (e.g. light/dark mode) -- that's when you want to look into a state management tool like Redux or Context.
There are two ways I can think of to achieve what you are trying to do here, i.e. get the child component's state in a parent component.
You can make use of refs. You should be familiar with React hooks to use this approach. The documentation is really good.
You can also make use of a global state using either Redux or Context.
I have below component which gets data expenses as props,
Initial reader will show table with expenses values, but when row is deleted new data are stored in newExpenses which need to use in DataGrid
const DataTable = ({ expenses }) => {
const handleDeleteCompleted = async () => {
// Function for delete
const newExpenses = await deleteExpense(fetchExpenseId)
setOpenConfirmDelete(false);
enqueueSnackbar(t('The user account has been removed'), {
variant: 'success',
anchorOrigin: {
vertical: 'top',
horizontal: 'right'
},
TransitionComponent: Zoom
});
};
return (
<>
<div style={{ height: 800, width: "100%" }}>
<DataGrid
rows={expenses}
columns={columns}
/>
</div>
</>
);
};
export default DataTable;
Now I want to update DataGrid rows with newExpenses which are fetch with deleteExpense function.
How can Irerender DataGrid with new data?
Thank youm
You're duplicating state and confusing where your system of record is. The first thing you need to do is decide:
Should the expenses state be managed in this component, or should it be managed in a parent and passed to this component?
Don't mix the two.
If the answer is that state should be managed in this component, remove the prop. Within this component you would fetch the data (perhaps once initially in a useEffect for example), store it in useState, and update that state accordingly (which will trigger a re-render).
If the answer is that state should be managed in a parent component, then that component needs to perform the updates/deletes/etc. It can pass functions to do that as additional props for this component to use. As that parent component updates its state, that would trigger a re-render of this child component.
When you need to rerender a component based on data, you need to put that data into state. Whenever React state changes, the component gets rerendered.
In your case you'd need to put the prop expenses inside state.
import { useState } from 'react';
const DataTable = ({ expenses: initialExpenses }) => {
const [expenses, setExpenses] = useState(initialExpenses);
...
// On handler
const newExpenses = await deleteExpense(fetchExpenseId);
setExpenses(newExpenses);
}
That will make sure that your component gets rerendered.
This is the case in which the prop is the initial data, because you can't update the components props, you can only use them. In this example, your component is managing the state.
In the case where the parent component needs to manage the state, you'd need to not only pass expenses as a prop, but a callback to update it. Whenever a React prop changes, the component also rerenders. In this case you'd pass in both expenses and onDeleteExpense (for example). The parent component is the one who defines onDeleteExpense and handles the state (using useState).
const Parent = () => {
const [expenses, setExpenses] = useState(someInitialValue);
const onDeleteExpense = (expenseId) => {
const newExpenses = await deleteExpense(expenseId);
setExpenses(newExpenses);
}
return <DataTable expenses={expenses} onDeleteExpenses={onDeleteExpenses} />;
}
I have a question about useRef: if I added ref.current into the dependency list of useEffect, and when I changed the value of ref.current, the callback inside of useEffect won't get triggered.
for example:
export default function App() {
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<div className="App">
<button
onClick={() => {
myRef.current = myRef.current + 1;
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
</div>
);
}
Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?
Also I know I can use useState here. This is not what I am asking. And also I know that ref stay referentially the same during re-renders so it doesn't change. But I am not doing something like
const myRef = useRef(1);
useEffect(() => {
//...
}, [myRef]);
I am putting the current value in the dep list so that should be changing.
I know I am a little late, but since you don't seem to have accepted any of the other answers I'd figure I'd give it a shot too, maybe this is the one that helps you.
Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?
Short answer, no.
The only things that cause a re-render in React are the following:
A state change within the component (via the useState or useReducer hooks)
A prop change
A parent render (due to 1. 2. or 3.) if the component is not memoized or otherwise referentially the same (see this question and answer for more info on this rabbit hole)
Let's see what happens in the code example you shared:
export default function App() {
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<div className="App">
<button
onClick={() => {
myRef.current = myRef.current + 1;
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
</div>
);
}
Initial render
myRef gets set to {current: 1}
The effect callback function gets registered
React elements get rendered
React flushes to the DOM (this is the part where you see the result on the screen)
The effect callback function gets executed, "myRef current changed" gets printed in the console
And that's it. None of the above 3 conditions is satisfied, so no more rerenders.
But what happens when you click the button? You run an effect. This effect changes the current value of the ref object, but does not trigger a change that would cause a rerender (any of either 1. 2. or 3.). You can think of refs as part of an "effect". They do not abide by the lifecycle of React components and they do not affect it either.
If the component was to rerender now (say, due to its parent rerendering), the following would happen:
Normal render
myRef gets set to {current: 1} - Set up of refs only happens on initial render, so the line const myRef = useRef(1); has no further effect.
The effect callback function gets registered
React elements get rendered
React flushes to the DOM if necessary
The previous effect's cleanup function gets executed (here there is none)
The effect callback function gets executed, "myRef current changed" gets printed in the console. If you had a console.log(myRef.current) inside the effect callback, you would now see that the printed value would be 2 (or however many times you have pressed the button between the initial render and this render)
All in all, the only way to trigger a re-render due to a ref change (with the ref being either a value or even a ref to a DOM element) is to use a ref callback (as suggested in this answer) and inside that callback store the ref value to a state provided by useState.
https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
use useCallBack instead, here is the explanation from React docs:
We didn’t choose useRef in this example because an object ref doesn’t
notify us about changes to the current ref value. Using a callback ref
ensures that even if a child component displays the measured node
later (e.g. in response to a click), we still get notified about it in
the parent component and can update the measurements.
Note that we pass [] as a dependency array to useCallback. This
ensures that our ref callback doesn’t change between the re-renders,
and so React won’t call it unnecessarily.
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
Ok so I think what you're missing here is that changing a ref's value doesn't cause a re-render. So if it doesn't cause re-renders, then the function doesn't get run again. Which means useEffect isn't run again. Which means it never gets a chance to compare the values. If you trigger a re-render with a state change you will see that the effect will now get run. So try something like this:
export default function App() {
const [x, setX] = useState();
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<button
onClick={() => {
myRef.current = myRef.current + 1;
// Update state too, to trigger a re-render
setX(Math.random());
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
);
}
Now you can see it will trigger the effect.
I have below composition:
const HookComponent = (props)=> {
let [month, changeMonth] = useState(moment());
return (
<ChildComponent
month={month}
onChange={m => changeMonth(m)}
minStep={5}
prevMonthIcon="ion-ios-arrow-left"
nextMonthIcon="ion-ios-arrow-right"
/>
)
}
The ChildComponent is a class component which updates the month using setState. Problem with above is that the change is not reflecting on DOM but the state in parent component is changing (via user input - button which changes the state in ChildComponent). I logged it and confirm the month in parent is changing. Is this some limitation of react when using class components within hooks?
When I convert HookComponent to class component and change month using setState, it works as expected and DOM changes on input change.
It seems that the InputMoment component does not use a month prop, but a moment one.
Also, it seems that InputMoment is returning the same moment instance that is passed as moment prop. This causes that when you execute the changeMonth statement, as the reference does not change, the element is not re-rendered.
You can solve this by storing an object in the state. When you call changeMonth you pass a new object, and the InputMoment is then re-rendered correctly:
const HookComponent = (props)=> {
let [month, changeMonth] = useState({ m: moment() });
return (
<ChildComponent
moment={month.m}
onChange={m => changeMonth({ m })}
minStep={5}
prevMonthIcon="ion-ios-arrow-left"
nextMonthIcon="ion-ios-arrow-right"
/>
)
}
I have a functional parent Map component with a few states, one of which is layers, the value of which is set initially from a config.
I then have a child LayerControl component which takes the layers state of the map as its props. The LayerControl component makes some changes and passes them back to the Map with onLayerChange which then subsequently calls the setState method of the layers (setLayers).
From what I understand, this should trigger a re-render of my LayerControl component, but it doesn't. What have I done wrong?
export default function Map(){
const [viewport, setViewport] = useState(mapConfig.viewport);
const [style, setStyle] = useState(mapConfig.mapStyle);
const [layers, setLayers] = useState(mapConfig.layers);
function updateLayers(layers){
setLayers(layers); //why won't LayerControl re-render?
}
return(
<MapGL
zoom={[viewport.zoom]}
containerStyle={{
height: "100%",
width: "100%"
}}
style={style}>
<LayerControl
onLayerChange={layers => updateLayers(layers)}
layers={layers}
/>
<ZoomControl/>
<ScaleControl/>
</MapGL>
);
}
Are you creating a new layers Array in the LayerController or are you just adding/removing an item to the layers array? I assume setLayers only does a shallow reference check to see if layers changed, which will return false if you modified the array instead of re-creating it. Thus your component won't re-render.
For example
// In LayerController
// this mutates the array, but won't trigger a render
layers.push(/* some new layer */);
onLayerChange(layers);
// creates a new array, which should trigger a render
const newLayer = {/* your new layer */};
const newLayers = [...layers, newLayer];
onLayerChange(newLayers)