ReactJS, props between child and parent in render loop - reactjs

I have this passing props code between child and its parent:
Parent.js
const defaultState = {
canOperate: false,
// among other states
};
const [state, setState] = useState(defaultState);
// this technique is for avoiding some re-renders in some occasions, I'm using a lot of this way to set the
// state in this Parent component, but isn't working on this case
const handleCanOperate = (value: boolean) => {
setState(state => ({
...state,
canOperate: value
}));
};
<ChildComponent
onCanOperate={handleCanOperate}
/>
Child.js
// from props I have: onCanOperate
useEffect(() => {
const handleCanOperate = (canOperate: boolean) => onCanOperate(canOperate);
if (data) {
handleCanOperate(false);
setState(state => ({
...state,
isDisabled: true
}));
} else {
setState(s => ({
...state,
isDisabled: false
}));
handleCanOperate(true);
}
}, [data, onCanOperate]);
With this approach I get a loop, but if in the parent I do this change there is no problem:
Parent that works:
const defaultState = {
// other states
};
const [state, setState] = useState(defaultState);
const [canOperate, setCanOperate] = useState(false);
<ChildComponent
onCanOperate={setCanOperate}
/>
I don't want to use two setters for state in the Parent component, that's why I put a handler to call the already working setState
Any idea?

I believe your loop is caused by the handleCanOperate function which gets redefined on re-render (when the state changes). This gets passed down to your child, which reruns the logic in your useEffect hook because it thinks onCanOperate has changed.
This also explains why it works when you place your handler in it's own state. React state is memoized and doesn't reinitialise on re-renders.
To fix this you could memoize your handleCanOperate so it doesn't reinitialise every re-render by wrapping it around a useCallback hook. This hook works similarly to the useEffect hook and will only re-initialise your callback when a value in it's dependency array changes.
Your parent would look something like this:
const defaultState = {
canOperate: false,
// among other states
};
const [state, setState] = useState(defaultState);
const handleCanOperate = useCallback((value: boolean) => {
setState(state => ({
...state,
canOperate: value
}));
}, []);
<ChildComponent
onCanOperate={handleCanOperate}
/>
I haven't tested this, but I do believe this would fix your issue.

Related

React using setState within useEffect() problem [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 5 months ago.
N.B. I got my answer here but it is not the duplicate question of this thread
I am trying to fetch data from a reusable function that has an API. Here is my code
usaData.js in another page
const useData = () => {
const [data, setData] = useState([]);
const fetchData = async (url, query, variable) => {
const queryResult = await axios.post(url, {
query: query,
variables: variable,
});
setData(queryResult.data.data);
};
return {data, fetchData}
};
I am retrieving data from this MainPage.js file
export const MainPage = props => {
const [state, setState] = useState({
pharam: 'Yes',
value: '',
});
const [field, setField] = useState([])
const {data, fetchData} = useData()
const onClick = (event) => {
setState({ ...state, pharam: '', value: event });
fetchData(url, query, event)
setField(data)
}
return (
<div>
...
<Select
placeholder='select'
>
{field.map(item => (
<Select.Option key={item.name}>
{item.name}
</Select.Option>
))}
</Select>
<Button onClick={onClick}>Change Select</Button>
...
</div>
)
}
The problem is setField(data) within onClick function is not updating immediately as it is a async call. Hence I tried to use a function as a second argument
...
setField(data, () => {
console.log(data)
})
...
It is returning the following warning in red color but the behavior is similar to earlier, not updating data immediately.
Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
As per the warning then I tried to use useEffect() within the onClick function
...
const onClick = (event) => {
setState({ ...state, pharam: '', value: event });
useEffect(() => {
fetchData(url, query, event)
setField(data)
}, [data])
}
...
which is returning an error
React Hook "useEffect" is called in function "onClick" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
Where do I have to make changes? How can I get expected behavior as the setField will update the field immediately?
My suggestion would be to not setState in your custom hook rather than return promise.
usaData.js
const useData = () => {
const fetchData = async (url, query, variable) => {
return await axios.post(url, {
query: query,
variables: variable,
});
};
return { fetchData };
};
In MainPage.js
Now when you trigger your onClick function just call your fetchData function with await or then syntax and after successfully api call you'll get back the result in the newData variable which you can use it to update your state.
Note: this will save you an extra useEffect.
export const MainPage = (props) => {
const [state, setState] = useState({
pharam: "Yes",
value: "",
});
const [field, setField] = useState([]);
const { fetchData } = useData();
const onClick = async (event) => {
setState({ ...state, pharam: "", value: event });
let newData = await fetchData(url, query, event);
console.log("===>", newData.data.data);
setField(newData.data.data);
};
return (
<div>
...
<Select placeholder="select">
{field.map((item) => (
<Select.Option key={item.name}>{item.name}</Select.Option>
))}
</Select>
<Button onClick={onClick}>Change Select</Button>
...
</div>
);
};
The problem in your case is that setField gets calls before your data is fetched.
So, you can have a useEffect which gets executed every time the data gets changed.
useEffect(() => {
if(data.length > 0) {
setField(data);
}
}, [data])
const onClick = (event) => {
setState(prev => ({ ...prev, pharam: '', value: event }));
fetchData(url, query, event);
}
As far I know, React sets its state asynchronously. So, in order to update the state Field, you need an useEffect hook. Your approch with useEffect is correct, except it neeed to be placed outside onClick (directly in the component function).
export const MainPage = () => {
...
useEffect(() => {
setField(data)
},[data])
...
}

Children components are not re rendered on forceUpdate() in React

I have a callback method that calls a forceUpdate() hook in parent. I expect this to re render and call ChildA with updated props. These values are updated in another component say ChildB.
While I keep a debugger at callback method, I see updated values for props and Im getting a hit to return method as well. But the child component is not getting hit at all.
const Body = FC =>{
const [state, setState] = useState<any>();
const forceUpdate = useForceUpdate();
const update = useCallback(() => forceUpdate(), []);
return (
//able to see updated state here when update() is called
//but execution is not going inside ChildA
//even use effects on updated state are not getting triggered
<ChildA
state = {state}
/>
<ChildB
update = {update}
/>
)
}
Existing hook:
const reducer = (state: boolean, _action: null): boolean => !state;
export const useForceUpdate = () => {
const [, dispatch] = useReducer(reducer, true);
// Turn dispatch(required_parameter) into dispatch().
const memoizedDispatch = useMemo(
() => () => {
dispatch(null);
},
[dispatch]
);
return memoizedDispatch;
};
When I changed the existing hook into the below format, this worked for me.
function useForceUpdate() {
const [, forceUpdate] = useReducer(x => x + 1, 0);
const memoizedUpdate = useMemo(
() => () => {
forceUpdate(0);
},
[forceUpdate]
);
return memoizedUpdate;
};

useReducer - Trying to write own useReducer which will not use dispatch if component is unmounted

could you provide your feedback on the code below:
export function useUnmountSafeReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>] {
const [mounted, setMounted] = useState(true);
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
return () => {
setMounted(false);
};
}, []);
return [state, mounted ? dispatch : () => {}];
}
I am trying to write own reducer which will not use dispatch if component is unmounted.
Try with a ref instead of a state.
const mounted = useRef(true)
useEffect(() => {
return () => {
mounted.current = false
}
}, [])
The reason is that using setMounted is a memory leak used in the destroy function of useEffect. Keep in mind if the component is unmounted, you are not supposed to use any internal method after that. Actually avoiding the memory leak is your reason to implement this mounted at the first place, isn't it?
disabled dispatch
Now the question is can you return a new dispatch after the unmount?
return [state, mounted ? dispatch : () => {}]
After the unmount, there probably won't be any more update to the UI . So the way to get it working is to disable the existing dispatch but not providing an empty one.
const _dispatch = useCallback((v) => {
if (!mounted || !mounted.current) return
dispatch(v)
}, [])
return [state, _dispatch]
The useCallback there might be optional.

Using two useeffect react hooks

I try to use react with hooks. I have this state:
const [state, setState] = useState<State>({
titel: "",
somethingElse: "",
tabledata: [],
});
I have two useeffect:
// componentDidMount, Initial variable deklaration
useEffect(() => {
//Do something
//Set initial states
setState({
...state,
titel: props.titel,
somethingElse: props.somethingElse,
})
}, []);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
//Do something and generate tabledata
let tabledata ....
//Set tabledata
setState({
...state,
tabledata: tabledata,
})
}, [props.taenzer]);
Now I have the behavior, that the second useeffect is overwriting the first useeffect setState command.
My variable titel and somethingElse is always empty.
Now I could change my deklaration of state, something in this way:
const [titel, setTitel] = useState<>({
titel = ""
});
const [somethingElse, setSomethingElse] = useState<>({
somethingElse = ""
});
But this makes the whole unclear and it is not so easy to set the state of several variables in one time like we could with setState(...state, titel="Hello", somethingElse="Else")
Any other possibility?
the second useeffect is overwriting the first useeffect setState
useState function doesn't automatically merge the state. So you would need to make use of the previous state accessible via callback.
useEffect(
() => {
const tabledata = ...
setState(prevState => ({ // use previous state
...prevState,
tabledata
}))
}, [props.taenzer]
)
Any other possibility?
You can have lazy initialization of your state via props and remove the first effect (without dependency)
const [{ title, somethingElse, tabledata }, setState] = useState<State>({
...props,
tabledata: [],
});

React custom hook not receiving current state of value from another custom hook

I have an event handler that sets a selected value in a list to [values, setValues] state hook. After doing this, I have a useEffect hook that calls an [options, setOptions] state hook to set that value to the options value. However, although setValues is able to set the state (which is shown when I console.log, setOptions is not able to set the new values state to the options state
UPDATE
Moving the console.log(options.data) out of the effect solves the problem partially because it logs out the current value, however, returning that value and using it in another custom hook returns the default value of options. I have updated the code.
I have tried using useRef to get the current value but my implementation is not working. Maybe I am doing that bit wrong.
const useFirstCustomHook = () => {
const [options, setOptions] = useState({
data: null
})
const [values, setValues] = useState({
name: "",
myid: "my-id"
});
//state changer
function handleChange(event, id) {
setValues(oldValues => ({
...oldValues,
name: event.target.value,
myid: id.props.id,
}));
}
useEffect(() => {
setOptions({
data: values
})
},[values])
console.log(options.data) //not printing the current state
return {
options
}
}
export default useFirstCustomHook
const [waitState, setWait] = useState(0)
const { values, options } = useFirstCustomHook ()
useEffect(() => {
console.log(options)
let wait = () => {
setTimeout(() => setWait(waitState+1), 10000)
}
async function fetchData() {
//fetch some data then wait
wait()
}
},[wait])
export default useFetchedData
I expect the new state of options to be the same as the current value of values but it is only reflecting the default value.

Resources