What to put in the dependency array in React.useMemo - javascript

Consider situation:
const App = ({ data = [], users = [] }) => {
const selectedId = React.memo(() => {
return data.find((id) => id === 'someId');
}, [data]);
const userIds = React.memo(() => {
return users.map(({ id }) => id);
}, [users]);
const finalIds = React.memo(() => {
return userIds.find((id) => id === selectedId);
}, []);
return finalIds.toString();
}
What should I put in the dependency array in finalIds?
Should I:
provide the props used by variables that are used here, e.g. - [data, users]
provide the variables that are used here, e.g. - [selectedId, userIds]
keep it empty

You put any variables that you use inside the calculation. In other words, [selectedId, userIds]. When those variables change, you need to repeat the calculation, or finalIds will be stale.
If you use eslint, i'd recommend using eslint-plugin-react-hooks. It can spot if you're missing a variable from the dependency array and fill them in automatically.

Related

How are you supposed to fire useEffect with more complex dependencies?

I have encountered the need to be able to add arrays and objects to my useEffect dependency array. What are the recommended approaches for doing this? For arrays I currently use the length of the array which is flawed because an update to the array where its length was constant would not change.
For complex objects too large to recreate with useMemo I am currently using the package useDeepEffect which is doing deep comparisons on the object. I have seen mention of converting it to JSON which is anathema to me. All of my implementations seem slightly hacky here please advise on some recommended way as I have yet encountered any tutorial with state more complex than a counter.
Using JSON.stringify():
const [items, setItems] = useState({id: 0, content: ""});
useEffect(() => {
const getItems = async () => {
const res = await axios.get(apiURL);
if (res && res.data) {
setItems(res.data);
}
}
getItems();
}, [JSON.stringify(items)]);
Using deep comparison with lodash:
// For putting object array in dependency array
function deepCompareEquals(prevVal, currentVal){
return _.isEqual(prevVal,currentVal );
}
function useDeepCompareWithRef(value) {
const ref = useRef();
if (!deepCompareEquals(value, ref.current)) {
ref.current = value; //ref.current contains the previous object value
}
return ref.current;
}
useEffect(() => {
const getItems = async () => {
const res = await axios.get(apiURL + '/items');
if (res && res.data) {
setTodos(res.data);
}
}
getItems();
}, [useDeepCompareWithRef(items)]);
For a more thorough discussion, read here: https://betterprogramming.pub/tips-for-using-reacts-useeffect-effectively-dfe6ae951421

Reactjs array mutation

I have this function:
setNotActiveWalletsList = () => {
const { GetAccounts } = this.props;
let shallowCopyOfWalletsArray = [...GetAccounts]
const notActive = shallowCopyOfWalletsArray.filter(user => user.active !== true);
let newArr = notActive.map(item => {
return decryptAccountInformation(item).then(result => {
!result.address ? null : item.address = result.address
})
});
this.setState({ onlyNotActive: newArr });
}
GetAccounts is an array of objects
The issue is, One of my colleagues have told me that I am mutating the array with this line:
!result.address ? null : item.address = result.address
But I don't really understand why is this considered a mutation? I am sure I created a copy of the original array and modified it.
Any suggestions on how to resolve this, please?
Spread syntax just does a one level closing of the object or array. Any object or array which is more than one level deep will still have the same reference. Hence when you using notActive array items, you are essentially working on the same reference that was inside GetAccounts
The correct way to update is to return the cloned and updated reference from within the map function and using Promise.all to also handle the async call
setNotActiveWalletsList = () => {
const { GetAccounts } = this.props;
let shallowCopyOfWalletsArray = [...GetAccounts]
const notActive = shallowCopyOfWalletsArray.filter(user => user.active !== true);
let promises = notActive.map(item => {
return decryptAccountInformation(item).then(result => {
return !result.address ? item : {...item, address: result.address}
})
});
Promise.all(promises).then(newArr => this.setState({ onlyNotActive: newArr }));
}

How do I use an updated value in useEffect

I'm new to Javascript and I'm currently experimenting with the Demo application out of the Docker getting-started tutorial. The application is a simple Todo list where you can add items and remove them.
I'm trying to update the list on every instance of the page without having to reload the page.
I've managed to edit the node express server so that it sends updates via Server-sent events.
The problem:
The frontend uses React. The data of the currently displayer items is contained in the ìtems array.
onNewItem adds items to that array. However when onNewItem is called from onmessage the items array is null even though it's not null when onNewItem is called from other React components. How can I access the initialized version of the items array? (It gets initialized by the 1. useEffect which fetches items from the server)
Below is a part of the code
function TodoListCard() {
const [items, setItems] = React.useState(null);
const [ listening, setListening ] = React.useState(false);
React.useEffect(() => {
fetch('/items')
.then(r => r.json())
.then(setItems);
}, []);
React.useEffect( () => {
if (!listening) {
const events = new EventSource('/events/subscribe');
events.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
switch (parsedData.type) {
case "add":
var newItem = {id: parsedData.id, name: parsedData.name, completed: parsedData.completed};
onNewItem(newItem);
break;
default:
break;
}
};
setListening(true);
}
}, [listening]);
const onNewItem = React.useCallback(
newItem => {
if (items.some(e => e.id === newItem.id)){return;}
setItems([...items, newItem]);
},
[items],
);
Let's start of by why things are going wrong. The issue is that when you call onNewItem(newItem) you are using an outdated reference to the onNewItem. For this reason items within the function will still be set to the initial value.
You partially solved this by providing an dependency array to React.useCallback. This will update onNewItem when a new value of items is available. However since React.useEffect does not list onNewItem as a dependency it keeps using the old version of onNewItem.
With this being said you might consider adding onNewItem, to the dependency array of React.useEffect. Although this is the correct action, just adding this to dependency array is not enough.
What is the problem you get when you add onNewItem to the depency array of React.useEffect? There is no cleanup function, so you will subscribe to the channel multiple times with different onmessage handlers (different versions of onNewItem).
So taking all the above into account a solution might look something like this:
function TodoListCard() {
const [items, setItems] = React.useState(null);
const [events, setEvents] = React.useState(null);
React.useEffect(() => {
const pEvents = fetch('/items')
.then(r => r.json())
.then(setItems)
.then(() => new EventSource('/events/subscribe'));
pEvents.then(setEvents);
return () => pEvents.then(events => events.close());
}, []);
React.useEffect(() => {
if (!events) return;
events.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
switch (parsedData.type) {
case "add":
var newItem = {
id: parsedData.id,
name: parsedData.name,
completed: parsedData.completed
};
onNewItem(newItem);
break;
default:
break;
}
};
}, [events, onNewItem]);
const onNewItem = React.useCallback(newItem => {
const isPresent = items.some(item => item.id === newItem.id);
if (isPresent) return;
setItems([...items, newItem]);
}, [items]);
return (
// ...
);
}
I've moved the EventSource creation inside the first React.useEffect since that only needs to happen once the component is mounted (and needs to close the connection on unmount). An empty dependency array will only call the function on mount, and calls the cleanup function on unmount.
The second React.useEffect now has the dependency array [events, onNewItem], because when events is set the onmessage handler needs to be attached. And if the onNewItem callback updates to a new version you should attach it as the new onmessage handler (replacing the old handler). This doesn't need a cleanup function anymore since, opening and closing events is already handled.
Although the above should do the job. If managing a specific state is becoming more complicated it might be better to opt for useReducer instead of useState.
function reducer(items, action) {
switch (action.type) {
case "add":
const isPresent = items.some(item => item.id == action.item.id);
if (isPresent) return items;
return [...items, action.item];
case "replace all":
return action.items;
case "complete": // <- unused example case
return items.map(item => {
if (item.id != action.id) return item;
return {...item, completed: true};
});
// ...
default: // silently ignore unsupported operations
return items;
}
}
function TodoListCard() {
const [items, dispatch] = React.useReducer(reducer, null);
React.useEffect(() => {
const pEvents = fetch('/items')
.then(r => r.json())
.then(items => dispatch({type: "replace all", items}))
.then(() => new EventSource('/events/subscribe'));
pEvents.then(events => {
events.onmessage = (event) => {
const {type, ...item} = JSON.parse(event.data);
dispatch({type, item});
};
});
return () => pEvents.then(events => events.close());
}, []);
// if you still need onNewItem for your render:
const onNewItem = React.useCallback(item => {
dispatch({type: "add", item});
}, []);
return (
// ...
);
}
The above extracts all the items management logic into a "reducer" function. The dispatch function returned by useReducer is guaranteed to be stable by React, so you can omit it from dependency arrays (but you don't have to).
The error that you have done is you are not getting any data from your api.The setItems in your first useEffect won't work.
Wrong Way:
React.useEffect(() => {
fetch('/items')
.then(r => r.json())
.then(setItems);
}, []);
Right Way:
useEffect(() => {
fetch('/items')
.then(r => r.json())
.then((result) => {
setItems(result.items)
});
}, []);

Filter through an array and return Params

I am trying to filter through an array of data from my API, however, the data returns unfiltered, I am thinking its something to do with the way I structured my method or It could be that my params are not being returned. I have below both my codes. I would really appreciate any effort. Thanks
Trying to filter data
const [catalogueArray, setCatalogueArray] = useState([])
useEffect(() => {
catalogueList()
}, [])
const catalogueList = () => {
const catalogue = data.filter(item => {
item.storeName == navigation.state.params.enteredStore
return { ...item }
})
setCatalogueArray(catalogue)
}
Setting Params
const handleSelectedStore = (name) => {
setSelectStore({ selectStore: name })
navigation.navigate('StoreCatalogue', { selectStore: name })
}
You are returning an object, not a boolean in filter(). Objects are truthy so you are effectively doing no filtering at all and are getting the same result as if you did return true
The == comparison above the return is doing nothing since you aren't using the result of that comparison anywhere
The equality check is what you want to return. If you want new objects in the array you need a map() for that in another step
const catalogue = data.filter(item => {
return item.storeName == navigation.state.params.enteredStore
}).map(item => ({ ...item }));

React issue with lodash and setInterval

I have an issue with using Lodash + setInterval.
What I want to do:
Retrieve randomly one element of my object every 3 seconds
this is my object:
const [table, setTable]= useState ([]);
so I start with that:
const result = _.sample(table);
console.log(result);
console give => Object { label: "Figue", labelEn: "fig" }
But if a add :
const result = _.sample(table);
console.log(result.label);
console give => TypeError: result is undefined
Beside that I tried to add setInterval and also try with useEffect but or code crash or console give me two numbers every 3 second => 2,6,2,6 ......
Ciao, supposing that table object is correclty filled, you could use lodash and setInterval to get random object of table using useEffect and useRef hooks like:
const interval = useRef(null);
useEffect(() => {
if (!interval.current) {
interval.current = setInterval(() => {
const result = _.sample(table);
console.log(result.label);
}, 3000);
}
}, [table]);
Then to clean setInterval when component will be unmount you could use another useEffect in this way:
useEffect(() => {
return () => {
clearInterval(interval.current);
interval.current = null;
};
}, []);
EDIT
After your explanation, I found the cause of your problem. Before start setInterval you have to wait that table was filled with values (otherwise you get an error).
To solve that it's just necessary to put another condition on first useEffect (table.length > 0), and load data on second useEffect.
So the first useEffect becomes:
const interval = useRef(null);
useEffect(() => {
if (!interval.current && table.length > 0) {
interval.current = setInterval(() => {
const result = _.sample(table);
console.log(result.label);
}, 3000);
}
}, [table]);
And the second one:
useEffect(() => {
jsonbin.get("/b/5f3d58e44d93991036184474/5")
.then(({data}) => setTable(data));
return () => {
clearInterval(interval.current);
interval.current = null;
};
}, []);
Here the codesandbox updated.
The problem is that you access table before it is loaded.
You should either provide an initial value to that allows a successful do all your operations:
// use an array that has at least one element and a label as initial table value
const [table, setTable] = useState([{label: "default label"}]);
useEffect(() => {
jsonbin.get("/b/5f3d58e44d93991036184474/5")
.then(({data}) => setTable(data));
});
const result = _.sample(table);
console.log(result.label);
// ...
Or use an if or something alike to handle multiple scenarios:
// use an empty array as initial table value
const [table, setTable] = useState([]);
useEffect(() => {
jsonbin.get("/b/5f3d58e44d93991036184474/5")
.then(({data}) => setTable(data));
});
const result = _.sample(table);
// account for the initial empty table value by checking the result
// of _.sample which is `undefined` for empty arrays
if (result) {
console.log(result.label);
} else {
console.log("do something else");
}
// ...
If you fetch your data asynchronously you must think about what you want to use while the data is being fetched. The minimal thing to do is tell React to not render the component while the data is missing (being fetch).
const [table, setTable] = useState([]);
useEffect(() => {
jsonbin.get("/b/5f3d58e44d93991036184474/5")
.then(({data}) => setTable(data));
});
const result = _.sample(table);
if (!result) return null; // <- don't render if there is no result
console.log(result.label);
// ...

Categories

Resources