Why doesn't useEffect run when sorting gets changed? I only want it to run when sorting.options get adjusted, but for some reason, when I add options to sorting.options the useEffect won't run happen; however, when I only have "sorting" it works perfectly even though only options get changed in handleChange? I dont want to have only sorting in there because I don't want the useEffect to run when sorting.value gets changed, for example.
const handleChange=index=>event=>{
let newOptions = sorting.options
newOptions[index]["value"]=event.target.value
setSorting((prevState)=>({
...prevState,
options:newOptions
}))
console.log("sorting changed")
}
useEffect(()=>{
console.log("Change happend")
},[sorting.options])//This works perfectly fine when only sorting is there, but when i put in sorting.options nothing happens
As Quentin said, try
let newOptions = sorting.options.slice(0)
To copy the options array instead of mutating it
The problem is that you keep the reference of the original object throughout different render. Just replace by a copy
import React, { useState, useEffect } from "react";
export default function Home() {
const [sorting, setSorting] = useState({
options: { 1: { value: 2 } },
email: "vapa#dieheli.us",
});
const handleChange = (index) => (event) => {
let newOptions = sorting.options; // Here you are getting the reference of sorting.opions which is constant between renders
// let newOptions = { ...sorting.options }; // Just replace previous line for a copy.
newOptions[index]["value"] = event.target.value; // Then you mutated the same object
setSorting((prevState) => ({
// setSorting creates a new object reference
...prevState,
options: newOptions, // but options references never changed
}));
};
useEffect(() => {
console.log("Change happend", sorting);
}, [sorting.options]);
return (
<div>
Home <input onChange={handleChange(1)} />
</div>
);
}
Related
I am using React and Material UI to create a table (XGrid) with some buttons. When you click the row, it should set the row id using useState. When you click the delete button, it should delete the row. It seems that the delete click handler is not using the value from use state. This is either some kind of closure thing or some kind of React thing.
const MyTableThing: React.FC = (props) =>
{
const { data } = props;
const [filename, setFilename] = React.useState<string>("")
const [columns, setColumns] = React.useState<GridColDef[]>([])
const handleDelete = () =>
{
someFunctionThatDeletes(filename); // filename is always ""
setFilename(""); // Does not do anything.. !
}
React.useEffect(() =>
{
if (data)
{
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) =>
{
if (column.field === "delete")
{
column.renderCell = (cellParams: GridCellParams) =>
{
return <Button onClick={handleDelete}>Delete</Button>
}
}
})
setColumns(columns)
}
}, [data?.files])
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) =>
{
console.log(`set selected row to ${param.data.id}`) // This works every time
setFilename(param.data.id)
}
}
The reason for this behavior is that React does not process setState action synchronously. It is stacked up with other state changes and then executed. React does this to improve performance of the application. Read following link for more details on this.
https://linguinecode.com/post/why-react-setstate-usestate-does-not-update-immediately
you can disable your deleteRow button till the filename variable is updated. you can use useEffect or setState with callback function.
useEffect(() => {
//Enable your delete row button, fired when filename is updated
}, filename)
OR
this.setFilename(newFilename, () => {
// ... enable delete button
});
Let me know if this helps! Please mark it as answer if it helps.
The main problem I see here is that you are rendering JSX in a useEffect hook, and then saving the output JSX into columns state. I assume you are then returning that state JSX from this functional component. That is a very bizarre way of doing things, and I would not recommend that.
However, this explains the problem. The JSX being saved in state has a stale version of the handleDelete function, so that when handleDelete is called, it does not have the current value of filename.
Instead of using the useEffect hook and columns state, simply do that work in your return statement. Or assign the work to a variable and then render the variable. Or better yet, use a useMemo hook.
Notice that we add handleDelete to the useMemo dependencies. That way, it will re-render every time handleDelete changes. Which currently changes every render. So lets fix that by adding useCallback to handleDelete.
const MyTableThing: React.FC = (props) => {
const { data } = props;
const [filename, setFilename] = React.useState<string>('');
const handleDelete = React.useCallback(() => {
someFunctionThatDeletes(filename); // filename is always ""
setFilename(''); // Does not do anything.. !
}, [filename]);
const columns = React.useMemo(() => {
if (!data) {
return null;
}
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) => {
if (column.field === 'delete') {
column.renderCell = (cellParams: GridCellParams) => {
return <Button onClick={handleDelete}>Delete</Button>;
};
}
});
return columns;
}, [data?.files, handleDelete]);
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) => {
console.log(`set selected row to ${param.data.id}`); // This works every time
setFilename(param.data.id);
};
return columns;
};
I verushc an array from one component to another component.
The initial array is filled by a DB and is not empty.
If I try to map over the array in my second component, it is empty (length = 0);
However, after I wrote a value in a search box to filter the array, all articles appear as intended.
What is that about?
export default function Einkäufe({ alleEinkäufe, ladeAlleEinkäufe, url }) {
const [searchTerm, setSearchTerm] = React.useState("");
const [searchResults, setSearchResults] = React.useState(alleEinkäufe);
const listeFiltern = (event) => {
setSearchTerm(event.target.value);
};
React.useEffect(() => {
setSearchResults(alleEinkäufe);
}, []);
React.useEffect(() => {
const results = alleEinkäufe.filter((eink) =>
eink.artikel.toLowerCase().includes(searchTerm.toLowerCase())
);
setSearchResults(results);
}, [searchTerm]);
[...]
{searchResults.map((artikel, index) => {
return ( ... );
})}
}
The problem is with your useEffect hook that sets the list of searchResults, it's not rerun when alleEinkäufe property is updated. You need to add alleEinkäufe as it's dependency.
React.useEffect(() => {
setSearchResults(alleEinkäufe);
}, [alleEinkäufe]);
My bet is that the parent component that renders Einkäufe is initially passing an empty array which is used as searchResults state and then never updated since useEffect with empty dependencies array is only run once on the component's mount.
I would also advise you to use English variable and function names, especially when you ask for assistance because it helps others to help you.
Your search term intially is "". All effects run when your components mount, including the effect which runs a filter. Initially, it's going to try to match any article to "".
You should include a condition to run your filter.
React.useEffect(() => {
if (searchTerm) {
const results = alleEinkäufe.filter((eink) =>
eink.artikel.toLowerCase().includes(searchTerm.toLowerCase())
);
setSearchResults(results);
}
}, [searchTerm]);
BTW, "" is falsy.
I needed a hook to get the previous distinct value of a specific state. It looks like this and it seems to work:
function usePreviousDistinct(state) {
const prevRef = useRef();
useEffect(() => {
prevRef.current = state;
}, [state]);
return prevRef.current;
}
I've also seen there is a usePreviousDistinct hook in the react-use package but the approach is different than mine.
import { useRef } from 'react';
import { useFirstMountState } from './useFirstMountState';
export type Predicate<T> = (prev: T | undefined, next: T) => boolean;
const strictEquals = <T>(prev: T | undefined, next: T) => prev === next;
export default function usePreviousDistinct<T>(value: T, compare: Predicate<T> = strictEquals): T | undefined {
const prevRef = useRef<T>();
const curRef = useRef<T>(value);
const isFirstMount = useFirstMountState();
if (!isFirstMount && !compare(curRef.current, value)) {
prevRef.current = curRef.current;
curRef.current = value;
}
return prevRef.current;
}
I wonder if I have not understood something or am missing something. Is my version also correct?
In my test I could not find a difference:
https://codesandbox.io/s/distracted-mayer-zpym8?file=/src/App.js
useEffect() together with useRef() (your version) does not show the latest value.
useRef() without useEffect() gives you the correct value, because it runs synchronously, but doesn't have the advantages that come with asynchronicity.
useEffect() together with useState() gives you the correct value, but might trigger unnecessary renders (adds potentially unnecessary overhead).
Your version looks like it works as expected, because the old value that is shown is the one that you expect to see as the new value. But the actual new value is not the one you want.
Example:
import React, { useState, useEffect, useRef } from 'react';
export const MyComponent = function(props){
const [state, setState ] = useState(0);
return <React.Fragment>
state: { state },<br />
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
<button onClick={ function(){
setState( state + 1 );
} }>
increment
</button>
</React.Fragment>;
};
const usePreviousDistinctUE = function( value ){
const prevRef = useRef();
useEffect(() => {
prevRef.current = value;
console.log('with useEffect, prev:', prevRef.current, ', current:', value);
}, [value]);
return prevRef.current;
};
const usePreviousDistinctR = function( value ){
const prevRef = useRef();
const curRef = useRef( value );
if( curRef.current !== value ){
prevRef.current = curRef.current;
curRef.current = value;
}
console.log('w/o useEffect, prev:', prevRef.current, ', current:', curRef.current);
return prevRef.current;
};
The values shown on the page are the same, but in the console they are different. That means the value in the useEffect() version is changed, it is only not yet shown on the page.
If you just add another hook that updates anything unrelated (leaving everything else unchanged), then the page (might*) magically show the updated value again, because the page is re-rendered and the previously already changed value is shown. The value is now wrong in your eyes, but it is not changed, only shown:
// ...
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
anything updated: { useAnythingUpdating( state ) },<br />
// ...
const useAnythingUpdating = function(state){
const [result, setResult ] = useState(0);
useEffect(() => {
setResult( state );
console.log('anything updated');
});
return result;
};
*But you shouldn't rely on something else triggering a re-render. I'm not even sure this would update as expected under all circumstances.
more Details:
useEffect() is triggered at some time when react decides that the prop must have been changed. The ref is changed then, but react will not 'get informed' about a ref change, so it doesn't find it necessary to re-render the page to show the changed value.
In the example without useEffect() the change happens synchronously.
React doesn't 'know' about this change either, but (if everything else runs as expected) there will be always a re-render when necessary anyway (you will have called that function from another function that is rendered at the end).
(Not informing react about the change is basically the point in using useRef(): sometimes you want just a value, under your own control, without react doing magic things with it.)
I am learning react hooks. I am having mock data js call "MockFireBase.js" as below:
const userIngredientsList = [];
export const Get = () => {
return userIngredientsList;
}
export const Post = (ingredient) => {
ingredient.id = userIngredientsList.length + 1;
userIngredientsList.push(ingredient);
return ingredient;
}
Then my react hooks component "Ingredients.js" will call this mock utilities as following details:
const Ingredients = () => {
const [userIngredients, setUserIngredients] = useState([]);
// only load one time
useEffect(() => { setUserIngredients(Get()); }, []);
const addIngredienHandler = ingredient => {
let responsData = Post(ingredient);
setUserIngredients(preIngredients => {
return [...preIngredients, responsData]
});
}
return (
<div className="App">
<IngredientForm onAddIngredient={addIngredienHandler} />
<section>
<IngredientList ingredients={userIngredients} />
</section>
</div>
);
)
}
When I added first ingredient, it added two (of course I get same key issue in console.log). Then I added second ingredient is fine.
If I remove the useEffect code as below, it will work good.
// only load one time
useEffect(() => { setUserIngredients(loadedIngredients); }, []);
I am wondering what I did anything wrong above, if I use useEffect
The problem is not in useEffect. It's about mutating a global userIngredientsList array.
from useEffect you set initial component state to be userIngredientsList.
Then inside addIngredienHandler you call Post(). This function does two things:
2a. pushes the new ingredient to the global userIngredientsList array`. Since it's the same instance as you saved in your state in step 1, your state now contains this ingredient already.
2a. Returns this ingredient
Then, addIngredienHandler adds this ingredient to the state again - so you end up having it in the state twice.
Fix 1
Remove userIngredientsList.push(ingredient); line from your Post function.
Fix 2
Or, if you need this global list of ingredients for further usage, you should make sure you don't store it in your component state directly, and instead create a shallow copy in your state:
useEffect(() => { setUserIngredients([...Get()]); }, []);
It is quite straight forward, when you press "add" it should add(and it adds) and when you press "remove" it should pop the last element and re-render the list but it doesn't. I am make mistake somewhere?
import React, { useState, useEffect } from 'react';
const Test = () => {
const [list, setList] = useState([]);
const add = () => {
setList([list.length, ...list]);
}
const remove = () => {
list.pop();
setList(list);
}
useEffect(() => {
console.log(list)
}, [list])
return (<ul>
<button onClick={add}>add</button>
<button onClick={remove}>remove</button>
{list.map(el => <li>{el}</li>)}
</ul>)
}
export default Test;
UPDATE:
Actually it updates the state by removing the last element but the re-render happen only when button "add" is pressed
It's not recommended to modify the state itself because it is immutable.
So instead using .pop() on the original state of the array, first I suggest to clone that one and remove the required element from there, then the result should passed to setList() function.
Try as the following instead:
const remove = () => {
const copy = [...list];
copy.pop();
setList(copy);
}
Think about the following:
const list = [1,3,5,6,7];
const copy = [...list];
copy.pop();
console.log(list);
console.log(copy);
I hope this helps!
You need to set a new array in this case, setList(list) will not cause React to re-render because it's still the same array you're using.
Try setList([...list]) in your remove function.
There's also an alternative to pop, and doesn't mutate the original variable:
const remove = () => {
const [removed, ...newList] = list
setList(newList)
}