Pass function via props cause useEffect infinite loop if I do not destructure props - reactjs

I have a parent component with a state. And I want to pass a handler to set some state from a child component.
This is my parent component.
function ParentComponent() {
const [filters, setFilters] = useState({});
const setFiltersHandler = useCallback(filtersObj => {
setFilters(filtersObj);
}, []);
useEffect(() => {
// Do something and pass this to <Content /> component
}, [filters]);
return (
<div>
<Content filters={filters}>
<SideBarFilters applyFilters={setFiltersHandler} />
</div>
);
}
And this is my child component. This causes infinit loop.
const SideBarFilters = props => {
const [filterForm, setFilterForm] = useState({
specialities: {value: "all"}
});
// Some code with a input select and the handler to set filterForm
useEffect(() => {
let filterObj = {};
for (let key in orderForm) {
filterObj = updateObject(filterObj, {
[key]: orderForm[key]["value"]
});
}
props.applyFilters(filterObj);
}, [props, orderForm]);
return <OtherComponent />;
};
But if I destructure the props, it does not loop. Like this
const SideBarFilters = ({applyFilters}) => {
// same code as before
useEffect(() => {
// same as before
applyFilters(filterObj);
}, [applyFilters, orderForm]);
return <OtherComponent />;
};
My guess is that has something to do with how React compare props.
Maybe I should memo all props. But I think that is not a pattern

props object is referentially different each time parent re-renders(and re-renders SideBarFilters).
You should not fight that. Trying to find workaround you may run into brand new issues with stale date.
Destructure as you do, it's expected and suggested way to deal with dependencies in hooks.

Related

does react "render" children before passing them to parent component?

I tried to create a "Loader" HOC that would show a loader until loaded, and then render children. But I get an error in the JSX that I'm passing to the Loader that it cant read .foo of null.
It seems like the children get "rendered" (ie string interpolated etc) before getting passed to the Loader HOC; I was hoping that the children would be passed as a function or something and then evaluated only if the HOC's conditional requires the rendering of its children. Is this not the case?
const Loader = ({
isLoading = false,
children,
}) => {
const [showLoader, setShowLoader] = React.useState(isLoading);
// when isLoading changes, update showLoader
React.useEffect(() => {
if(isLoading !== showLoader) {
setShowLoader(isLoading);
}
}, [isLoading]);
console.log(`Loader rendering with isLoading: ${isLoading} and showLoader: ${showLoader}`)
return showLoader ? (<div> Loading... </div>) : children;
}
const App = () => {
const [isLoading, setIsLoading] = React.useState(true);
const [recordData, setRecordData] = React.useState(null);
React.useEffect(() => {
// after 3 seconds, stop showing loader
console.log('setting state...');
setRecordData({ foo: 'bar '});
setTimeout(() => setIsLoading(false), 3000);
}, []);
return (
<div>
<Loader isLoading={isLoading}>
<div>
<p>This is the App, foo is {recordData.foo}</p>
</div>
</Loader>
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
It does not matter when components render. The properties are calculated right in the moment where code finds them. recordData.foo is just an expression which is calculated by interpretator and pass calculated value as a child of p-element. So naturally at first rendering recordData is null, which causes error.

Using React hooks, how can I update an object that is being passed to a child via props?

The parent component contains an array of objects.
It maps over the array and returns a child component for every object, populating it with the info of that object.
Inside each child component there is an input field that I'm hoping will allow the user to update the object, but I can't figure out how to go about doing that.
Between the hooks, props, and object immutability, I'm lost conceptually.
Here's a simplified version of the parent component:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(()=>{
// makes an axios call and triggers setCategories() with the response
}
return(
categories.map((element, index) => {
return(
<Child
key = {index}
id = {element.id}
firstName = {element.firstName}
lastName = {element.lastName}
setCategories = {setCategories}
})
)
}
And here's a simplified version of the child component:
const Child = (props) => {
return(
<h1>{props.firstName}</h1>
<input
defaultValue = {props.lastName}
onChange={()=>{
// This is what I need help with.
// I'm a new developer and I don't even know where to start.
// I need this to update the object's lastName property in the parent's array.
}}
)
}
Maybe without knowing it, you have lifted the state: basically, instead of having the state in the Child component, you keep it in the Parent.
This is an used pattern, and there's nothing wrong: you just miss a handle function that allows the children to update the state of the Parent: in order to do that, you need to implement a handleChange on Parent component, and then pass it as props to every Child.
Take a look at this code example:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(() => {
// Making your AXIOS request.
}, []);
const handleChange = (index, property, value) => {
const newCategories = [...categories];
newCategories[index][property] = value;
setCategories(newCategories);
}
return categories.map((c, i) => {
return (
<Child
key={i}
categoryIndex={i}
firstName={c.firstName}
lastName={c.lastName}
handleChange={handleChange} />
);
});
}
const Child = (props) => {
...
const onInputChange = (e) => {
props.handleChange(props.categoryIndex, e.target.name, e.target.value);
}
return (
...
<input name={'firstName'} value={props.firstName} onChange={onInputChange} />
<input name={'lastName'} value={props.lastName} onChange={onInputChange} />
);
}
Few things you may not know:
By using the attribute name for the input, you can use just one handler function for all the input elements. Inside the function, in this case onInputChange, you can retrieve that information using e.target.name;
Notice that I've added an empty array dependecies in your useEffect: without it, the useEffect would have run at EVERY render. I don't think that is what you would like to have.
Instead, I guest you wanted to perform the request only when the component was mount, and that is achievable with n empty array dependecies;

Using state setter as prop with react hooks

I'm trying to understand if passing the setter from useState is an issue or not.
In this example, my child component receives both the state and the setter to change it.
export const Search = () => {
const [keywords, setKeywords] = useState('');
return (
<Fragment>
<KeywordFilter
keywords={keywords}
setKeywords={setKeywords}
/>
</Fragment>
);
};
then on the child I have something like:
export const KeywordFilter: ({ keywords, setKeywords }) => {
const handleSearch = (newKeywords) => {
setKeywords(newKeywords)
};
return (
<div>
<span>{keywords}</span>
<input value={keywords} onChange={handleSearch} />
</div>
);
};
My question is, should I have a callback function on the parent to setKeywords or is it ok to pass setKeywords and call it from the child?
There's no need to create an addition function just to forward values to setKeywords, unless you want to do something with those values before hand. For example, maybe you're paranoid that the child components might send you bad data, you could do:
const [keywords, setKeywords] = useState('');
const gatedSetKeywords = useCallback((value) => {
if (typeof value !== 'string') {
console.error('Alex, you wrote another bug!');
return;
}
setKeywords(value);
}, []);
// ...
<KeywordFilter
keywords={keywords}
setKeywords={gatedSetKeywords}
/>
But most of the time you won't need to do anything like that, so passing setKeywords itself is fine.
why not?
A setter of state is just a function value from prop's view. And the call time can be anytime as long as the relative component is live.

Calling 'setState' of hook within context sequentially to store data resulting in race condition issues

I've created a context to store values of certain components for display elsewhere within the app.
I originally had a single display component which would use state when these source components were activated, but this resulted in slow render times as the component was re-rendered with the new state every time the selected component changed.
To resolve this I thought to create an individual component for each source component and render them with initial values and only re-render when the source components values change.
i.e. for the sake of an example
const Source = (props) => {
const { name, some_data} = props;
const [setDataSource] = useContext(DataContext);
useEffect(() => {
setDataSource(name, some_data)
}, [some_data]);
return (
...
);
}
const DataContextProvider = (props) => {
const [currentState, setState] = useState({});
const setDataSource = (name, data) => {
const state = {
...currentState,
[name]: {
...data
}
}
}
return (
...
)
}
// In application
<Source name="A" data={{
someKey: 0
}}/>
<Source name="B" data={{
someKey: 1
}}/>
The state of my provider will look like so;
{
"B": {
"someKey": 1
}
}
I believe this is because setState is asynchronous, but I can't think of any other solution to this problem
You can pass the function to setState callback:
setState((state) => ({...state, [name]: data}))
It takes the latest state in argument in any case, so it always safer to use if your update depends on previous state.

FlatList not updating with React Hooks and Realm

I am writing a custom hook to use it with realm-js.
export default function useRealmResultsHook<T>(query, args): Array<T> {
const [data, setData] = useState([]);
useEffect(
() => {
function handleChange(newData: Array<T>) {
// This does not update FlatList, but setData([...newData]) does
setData(newData);
}
const dataQuery = args ? query(...args) : query();
dataQuery.addListener(handleChange);
return () => {
dataQuery.removeAllListeners();
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[query, ...args]
);
return data;
}
In my component:
const MyComponent = (props: Props) => {
const data = useRealmResultsHook(getDataByType, [props.type]);
return (
<View>
<Text>{data.length}</Text>
<FlatList
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
/>
</View>
);
};
In the previous component, when doing setData(newData), the data.length gets updated correctly inside the Text. However, the FlatList does not re-render, like the data did not change.
I used a HOC before and a render prop with same the behavior and it was working as expected. Am I doing something wrong? I'd like to avoid cloning data setData([...newData]); because that can be a big amount of it.
Edit 1
Repo to reproduce it
https://github.com/ferrannp/realm-react-native-hooks-stackoverflow
The initial data variable and the newData arg in the handler are the links to the same collection. So they are equal and setData(newData) won’t trigger component’s re-render in this case.
It might be helpful to map Realm collection to the array of items’ ids. So, you will always have the new array in the React state and render will occur properly. It's also useful to check only deletions and insertions of the collection to avoid extra re-renders of the list. But in this case, you should also add listeners to the items.
function useRealmResultsHook(collection) {
const [data, setData] = useState([]);
useEffect(
() => {
function handleChange(newCollection, changes) {
if (changes.insertions.length > 0 || changes.deletions.length > 0) {
setData(newCollection.map(item => item.id));
}
}
collection.addListener(handleChange);
return () => collection.removeListener(handleChange);
},
[]
);
return data;
}

Resources