I have a function component:
// MovieOverview.tsx
const MovieOverview = () => {
const {data} = useQuery(resolvers.queries.ReturnAllMovies);
const movie: IMovie = useReactiveVar(moviesVar);
let movies: IMovie[] = data?.movies;
let movieRows;
const setMoviesInRow = (movies: IMovie[]) => {
const numberOfMovies = Math.floor((window.innerWidth -24) / 185);
return chunk(movies, numberOfMovies);
};
useEffect(() => {
movie ? movies = movies?.concat(movie) : null;
movies ? movieRows = setMoviesInRow(movies) : null;
movies ? console.log(movieRows) : null;
}, [movies, movie]);
return (
<div>
{movieRows?.length}
</div>
);
};
The console.log(movieRows) shows what I expect, an array with some objects. But in the template movieRows is undefined.
Setting a variable (movieRows = setMoviesInRow(movies)) doesn't cause a render, which is why you don't see it. In order to cause a render using React Hooks you must use a "set" function that is returned from the hook (like what is returned from useState). This function updates the variable and triggers a render.
For your purpose, useQuery doesn't return a "set" function (since you shouldn't need to change the return values of a query). What you should do is use window.innerWidth as a state variable that will update with its own useEffect - something like this example. You might be able to use an existing package like this so you don't have to write the hook yourself.
Then you can use it in your component without another useEffect:
// MovieOverview.tsx
const MovieOverview = () => {
const windowInnerWidth: Number = useWindowInnerWidth();
const {data} = useQuery(resolvers.queries.ReturnAllMovies);
const movie: IMovie = useReactiveVar(moviesVar);
let movies: IMovie[] = data?.movies;
let movieRows;
movie ? movies = movies?.concat(movie) : null;
const numberOfMovies = Math.floor((windowInnerWidth -24) / 185);
movies ? movieRows = chunk(movies, numberOfMovies) : null;
return (
<div>
{movieRows?.length}
</div>
);
};
The component will be re-rendered once when the window is ready and again when the query results arrive.
Related
I recently started using useEffect hook, and I'm facing some issues. This is a simple App component that renders an input field and submit button. On hitting submit selectItem is called which in turn calls an async function getNames(). getNames function checks if there is an existing entry, if so it returns, otherwise it calls 3rd party API getNewNames() to get newNames. I tried setting the state with this newNames field, but it seems like it is undefined in first render. But after the first render it is defined. How do I make sure that I have newNames field, so that it doesn't return undefined in any renders?
const App = () => {
const [namesArr, setNamesArr] = useState([])
const [name, setName] = useState('')
useEffect (()=> {
console.log('Inside use Effect')
}, [namesArr])
const changeInput = (val) => {
setName(val)
}
const selectItem = async() => {
const returnedVal = await getNames()
// ReturnVal is empty in first render, but filled in second render.
/
}
const getNames = async() =>{
const existingNames = namesArr.find((name)=> name === name)
if(existingNames){
return 'We have an entry'
}
else{
console.log(`Names are not reloaded properly, need to re-render`)
const newNames = await getNewNames() // this is
setName((oldNames)=> [...oldNames, newNames])
return namesArr
}
}
return <div>
<input value={name} onChange={(e)=> changeInput(e.target.value)}></input>
<button onClick={()=> selectItem()}></button>
</div>
}
I think your problem is related to the setName method. In the line after setName console.log(namesArr) will be undefined. So how can we fix this?
const getNames = async() =>{
const existingNames = namesArr.find((name)=> name === name)
if(existingNames){
return 'We have an entry'
}
else{
const newNames = await getNewNames();
// We created our new list outside
// If x is a list, prepend an ellipsis ...newNames
const newList = [...namesArr, newNames];
// setName string is a state. Incorrect state updating.
// setNamesArr instead of setName
setNamesArr(newList)
return newList;
}
}
Now there are two possibilities here.
returnedVal === array or returnedVal === 'We have an entry'
const selectItem = async() => {
const returnedVal = await getNames();
console.log(returnedVal); // array or string.
}
I'm retrieving some data from an API using useEffect, and I want to be able to filter that returned data using a prop being fed into the component from its parent.
I'm trying to filter the state after it is set by useEffect, however it looks like the component is going into an infinite render loop.
What do I need to do to prevent this?
export default function HomeJobList(props: Props): ReactElement {
const [listings, setListings] = React.useState(null);
useEffect(() => {
const func = async () => {
let res = await service.getListings();
setListings(res);
};
func();
}, []);
if (props.searchTerm && listings) {
let filtered = listings.filter((x) => x.positionTitle.includes(props.searchTerm));
setListings(filtered);
}
return (
<>
<div>do stuff</div>
</>
);
}
I understand that the use of the setListing function is then causing a rerender after the filtering, which then causes another setListing call. But what's the best way to break this loop?
Should I just have another state value that maintains the last searchTerm used to filter and check against that before filtering?
Or is there a better way?
It's an indinite loop because every time you filter, you set it as a state variable, which causes re-rendering and filtering & setting the variable again - thus a loop.
I suggest you do it all in one place (your useEffect is a good place for that, because it only executes once.
useEffect(() => {
(async () => {
const res = await service.getListings();
const filtered = res.filter((x) => x.positionTitle.includes(props.searchTerm));
setListings(filtered);
})();
}, []);
When the state changes that trigger a re-render, that's why you have an infinite loop; what you have to do is to wrap your filtering in a useEffectthat that depends on the searchTerm prop, like this:
import React, { useEffect } from 'react';
export default function HomeJobList() {
const [listings, setListings] = React.useState(null);
useEffect(() => {
const func = async () => {
let res = await service.getListings();
setListings(res);
};
func();
}, []);
useEffect(() => {
if (listings) {
let filtered = listings.filter(x =>
x.positionTitle.includes(props.searchTerm)
);
setListings(filtered);
}
}, [props.searchTerm]);
return (
<>
<div>do stuff</div>
</>
);
}
You need to create a function that's called inside the JSX you're returning.
Actually, you'll need to render the component every time the Props objects changes. That's achieved by calling the function in the JSX code.
Example:
Function:
const filteredListings = () => {
if (props.searchTerm && listings) {
let filtered = listings.filter((x) => {
x.positionTitle.includes(props.searchTerm));
}
return filtered;
}
}
Return Statement:
return (
<ul>
{
filteredListings().map((listing) =>
<li>{listing.title}</li>
);
}
</ul>
);
What you need is a useEffect which does the filtering when props changes.
Replace this
if (props.searchTerm && listings) {
let filtered = listings.filter((x) => x.positionTitle.includes(props.searchTerm));
setListings(filtered);
}
with
useEffect(()=>{
if (props.searchTerm) {
setListings(prevListing => {
return prevListing.filter((x) => x.positionTitle.includes(props.searchTerm))
});
}
}, [props.searchTerm] )
I am making a card system that can swap cards using drag and drop. When I'm dragging the cards over each other my state is updating as it's supposed to but it's not re-rendering.
import React, {useRef, useState} from 'react';
import DnDGroup from './DndGroup';
const DragNDrop = ({data}) => {
const [list, setList] = useState(data);
const dragItem = useRef();
const dragNode = useRef();
const getParams = (element) => {
const itemNum = element.toString();
const group = list.find(group => group.items.find(item => item === itemNum))
const groupIndex = list.indexOf(group);
const itemIndex = group.items.indexOf(itemNum);
return {'groupIndex': groupIndex, 'itemIndex': itemIndex}
}
const handleDragstart = (e) => {
dragNode.current = e.target;
setTimeout(() => {
dragNode.current.classList.add('current')
}, 0)
dragItem.current = getParams(e.target.value);
dragNode.current.addEventListener('dragend', () => {
if(dragNode.current !== undefined) {
dragNode.current.classList.remove('current')
}
})
}
const handleDragEnter = e => {
if(dragNode.current !== e.target && e.target.value !== undefined) {
const node = getParams(e.target.value);
const currentItem = dragItem.current;
[data[currentItem.groupIndex].items[currentItem.itemIndex], data[node.groupIndex].items[node.itemIndex]] = [data[node.groupIndex].items[node.itemIndex], data[currentItem.groupIndex].items[currentItem.itemIndex]];
setList(data);
console.log(list)
}
}
return (
<div className='drag-n-drop'>
{list.map(group => (
<DnDGroup
key={group.title}
group={group}
handleDragstart={handleDragstart}
handleDragEnter={handleDragEnter}
/>
))}
</div>
);
};
export default DragNDrop;
I also tried to do this:
setList([...data])
Using this it renders according to the state changes and works great inside one group, but when I want to drag a card to the other group, the state constantly changes back and forth like crazy, it also gives tons of console.logs.
How can I fix this?
With this line:
[data[currentItem.groupIndex].items[currentItem.itemIndex], data[node.groupIndex].items[node.itemIndex]] = [data[node.groupIndex].items[node.itemIndex], data[currentItem.groupIndex].items[currentItem.itemIndex]];
you're mutating a deeply nested object inside the data array. Never mutate state in React; it can result in components not re-rendering when you expect them to, in addition to other unexpected (and undesirable) side-effects.
Another thing that looks like it's probably an unintentional bug is that you're changing the data (original state) instead of the stateful list array.
Change it to:
const itemA = list[node.groupIndex].items[node.itemIndex];
const itemB = list[currentItem.groupIndex].items[currentItem.itemIndex];
const newList = [...list];
newList[currentItem.groupIndex] = {
...newList[currentItem.groupIndex],
items: newList[currentItem.groupIndex].map(
(item, i) => i !== currentItem.itemIndex ? item : itemB
)
};
newList[node.groupIndex] = {
...newList[node.groupIndex],
items: newList[node.groupIndex].map(
(item, i) => i !== node.itemIndex ? item : itemA
)
};
setList(newList);
This avoids the mutation of the nested items subarray.
Playing with React those days. I know that calling setState in async. But setting an initial value like that :
const [data, setData] = useState(mapData(props.data))
should'nt it be updated directly ?
Bellow a codesandbox to illustrate my current issue and here the code :
import React, { useState } from "react";
const data = [{ id: "LION", label: "Lion" }, { id: "MOUSE", label: "Mouse" }];
const mapData = updatedData => {
const mappedData = {};
updatedData.forEach(element => (mappedData[element.id] = element));
return mappedData;
};
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
export default function App() {
const [loadedData, setLoadedData] = useState(data);
const [filter, setFilter] = useState("");
const filterData = () => {
return loadedData.filter(element =>
filter ? element.id === filter : true
);
};
//loaded comes from a useEffect http call but for easier understanding I removed it
return (
<div className="App">
<button onClick={() => setFilter("LION")}>change filter state</button>
<ChildComponent dataProp={filterData()} />
</div>
);
}
So in my understanding, when I click on the button I call setFilter so App should rerender and so ChildComponent with the new filtered data.
I could see it is re-rendering and mapData(updatedData) returns the correct filtered data BUT ChildComponent keeps the old state data.
Why is that ? Also for some reason it's rerendering two times ?
I know that I could make use of useEffect(() => setMappedData(mapData(dataProp)), [dataProp]) but I would like to understand what's happening here.
EDIT: I simplified a lot the code, but mappedData in ChildComponent must be in the state because it is updated at some point by users actions in my real use case
https://codesandbox.io/s/beautiful-mestorf-kpe8c?file=/src/App.js
The useState hook gets its argument on the very first initialization. So when the function is called again, the hook yields always the original set.
By the way, you do not need a state there:
const ChildComponent = ({ dataProp }) => {
//const [mappedData, setMappedData] = useState(mapData(dataProp));
const mappedData = mapData(dataProp);
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
EDIT: this is a modified version in order to keep the useState you said to need. I don't like this code so much, though! :(
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
let actualMappedData = mappedData;
useMemo(() => {
actualMappedData =mapData(dataProp);
},
[dataProp]
)
console.log("** Render Child Component **");
return Object.values(actualMappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Your child component is storing the mappedData in state but it never get changed.
you could just use a regular variable instead of using state here:
const ChildComponent = ({ dataProp }) => {
const mappedData = mapData(dataProp);
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Since React hooks rely on the execution order one should generally not use hooks inside of loops. I ran into a couple of situations where I have a constant input to the hook and thus there should be no problem. The only thing I'm wondering about is how to enforce the input to be constant.
Following is a simplified example:
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = constantIdArray.map((id) => useCallback(() => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
}));
return { state, callbacks };
}
const idArray = ['id-1', 'id-2', 'id-3'];
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput(idArray);
return (
<div>
<div onClick={callbacks[0]}>
{state[0]}
</div>
<div onClick={callbacks[1]}>
{state[1]}
</div>
<div onClick={callbacks[2]}>
{state[2]}
</div>
</div>
)
}
Is there a pattern for how to enforce the constantIdArray not to change? My idea would be to use a creator function for the hook like this:
const createUseHookWithConstantInput = (constantIdArray) => () => {
...
}
const idArray = ['id-1', 'id-2', 'id-3'];
const useHookWithConstantInput = createUseHookWithConstantInput(idArray)
const SomeComponent = () => {
const { state, callbacks } = useHookWithConstantInput();
return (
...
)
}
How do you solve situations like this?
One way to do this is to use useEffect with an empty dependency list so it will only run once. Inside this you could set your callbacks and afterwards they will never change because the useEffect will not run again. That would look like the following:
const useHookWithConstantInput = (constantIdArray) => {
const [state, changeState] = useState({});
const [callbacks, setCallbacks] = useState([]);
useEffect(() => {
changeState(buildInitialState(constantIdArray));
const callbacksArray = constantIdArray.map((id) => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
});
setCallbacks(callbacksArray);
}, []);
return { state, callbacks };
}
Although this will set two states the first time it runs instead of giving them initial values, I would argue it's better than building the state and creating new callbacks everytime the hook is run.
If you don't like this route, you could alternatively just create a state like so const [constArray, setConstArray] = useState(constantIdArray); and because the parameter given to useState is only used as a default value, it'll never change even if constantIdArray changes. Then you'll just have to use constArray in the rest of the hook to make sure it'll always only be the initial value.
Another solution to go for would be with useMemo. This is what I ended up implementing.
const createCallback = (id, changeState) => () => {
const newState = buildNewState(id, constantIdArray);
changeState(newState);
};
const useHookWithConstantInput = (constantIdArray) => {
const initialState = buildInitialState(constantIdArray);
const [state, changeState] = useState(initialState);
const callbacks = useMemo(() =>
constantIdArray.map((id) => createCallback(id, changeState)),
[],
);
return { state, callbacks };
};