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;
}
Related
I have created the following context provider. In sort it's a toast generator. It can have multiple toasts visible at the same time.
It all worked great and such until I realized that the <Component/> further down the tree that called the const context = useContext(ToastContext) aka the consumer of this context and the creator of the toast notifications, was also re-rendering when the providerValue was changing.
I tried to prevent that, changing the useMemo to a useState hook for the providerValue, which did stop my re-rendering problem , but now I could only have 1 toast active at a time (because toasts was never updated inside the add function).
Is there a way to have both my scenarios?
export const withToastProvider = (Component) => {
const WithToastProvider = (props) => {
const [toasts, setToasts] = useState([])
const add = (toastSettings) => {
const id = generateUEID()
setToasts([...toasts, { id, toastSettings }])
}
const remove = (id) => setToasts(toasts.filter((t) => t.id !== id))
// const [providerValue] = useState({ add, remove })
const providerValue = React.useMemo(() => {
return { add, remove }
}, [toasts])
const renderToasts = toasts.map((t, index) => (
<ToastNote key={t.id} remove={() => remove(t.id)} {...t.toastSettings} />
))
return (
<ToastContext.Provider value={providerValue}>
<Component {...props} />
<ToastWrapper>{renderToasts}</ToastWrapper>
</ToastContext.Provider>
)
}
return WithToastProvider
}
Thank you #cbdeveloper, I figured it out.
The problem was not on my Context but on the caller. I needed to use a useMemo() there to have memoized the part of the component that didnt need to update.
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.
I'd like to start a discussion on the recommended approach for creating callbacks that take in a parameter from a component created inside a loop.
For example, if I'm populating a list of items that will have a "Delete" button, I want the "onDeleteItem" callback to know the index of the item to delete. So something like this:
const onDeleteItem = useCallback(index => () => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<div>
<span>{item}</span>
<button type="button" onClick={onDeleteItem(index)}>Delete</button>
</div>
)}
</div>
);
But the problem with this is that onDeleteItem will always return a new function to the onClick handler, causing the button to be re-rendered, even when the list hasn't changed. So it defeats the purpose of useCallback.
I came up with my own hook, which I called useLoopCallback, that solves the problem by memoizing the main callback along with a Map of loop params to their own callback:
import React, {useCallback, useMemo} from "react";
export function useLoopCallback(code, dependencies) {
const callback = useCallback(code, dependencies);
const loopCallbacks = useMemo(() => ({map: new Map(), callback}), [callback]);
return useCallback(loopParam => {
let loopCallback = loopCallbacks.map.get(loopParam);
if (!loopCallback) {
loopCallback = (...otherParams) => loopCallbacks.callback(loopParam, ...otherParams);
loopCallbacks.map.set(loopParam, loopCallback);
}
return loopCallback;
}, [callback]);
}
So now the above handler looks like this:
const onDeleteItem = useLoopCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
This works fine but now I'm wondering if this extra logic is really making things faster or just adding unnecessary overhead. Can anyone please provide some insight?
EDIT:
An alternative to the above is to wrap the list items inside their own component. So something like this:
function ListItem({key, item, onDeleteItem}) {
const onDelete = useCallback(() => {
onDeleteItem(key);
}, [onDeleteItem, key]);
return (
<div>
<span>{item}</span>
<button type="button" onClick={onDelete}>Delete</button>
</div>
);
}
export default function List(...) {
...
const onDeleteItem = useCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<ListItem key={index} item={item} onDeleteItem={onDeleteItem} />
)}
</div>
);
}
Performance optimizations always come with a cost. Sometimes this cost is lower than the operation to be optimized, sometimes is higher. useCallback it's a hook very similar to useMemo, actually you can think of it as a specialization of useMemo that can only be used in functions. For example, the bellow statements are equivalents
const callback = value => value * 2
const memoizedCb = useCallback(callback, [])
const memoizedWithUseMemo = useMemo(() => callback, [])
So for now on every assertion about useCallback can be applied to useMemo.
The gist of memoization is to keep copies of old values to return in the event we get the same dependencies, this can be great when you have something that is expensive to compute. Take a look at the following code
const Component = ({ items }) =>{
const array = items.map(x => x*2)
}
Uppon every render the const array will be created as a result of a map performed in items. So you can feel tempted to do the following
const Component = ({ items }) =>{
const array = useMemo(() => items.map(x => x*2), [items])
}
Now items.map(x => x*2) will only be executed when items change, but is it worth? The short answer is no. The performance gained by doing this is trivial and sometimes will be more expensive to use memoization than just execute the function each render. Both hooks(useCallback and useMemo) are useful in two distinct use cases:
Referencial equality
When you need to ensure that a reference type will not trigger a re render just for failing a shallow comparison
Computationally expensive operations(only useMemo)
Something like this
const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))}
Now you have a reason to memoized the operation and lazily retrieve the serializedValue everytime props.item changes:
const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item])
Any other use case is almost always worth to just re compute all values again, React it's pretty efficient and aditional renders almost never cause performance issues. Keep in mind that sometimes your efforts to optimize your code can go the other way and generate a lot of extra/unecessary code, that won't generate so much benefits (sometimes will only cause more problems).
The List component manages it's own state (list) the delete functions depends on this list being available in it's closure. So when the list changes the delete function must change.
With redux this would not be a problem because deleting items would be accomplished by dispatching an action and will be changed by a reducer that is always the same function.
React happens to have a useReducer hook that you can use:
import React, { useMemo, useReducer, memo } from 'react';
const Item = props => {
//calling remove will dispatch {type:'REMOVE', payload:{id}}
//no arguments are needed
const { remove } = props;
console.log('component render', props);
return (
<div>
<div>{JSON.stringify(props)}</div>
<div>
<button onClick={remove}>REMOVE</button>
</div>
</div>
);
};
//wrap in React.memo so when props don't change
// the ItemContainer will not re render (pure component)
const ItemContainer = memo(props => {
console.log('in the item container');
//dispatch passed by parent use it to dispatch an action
const { dispatch, id } = props;
const remove = () =>
dispatch({
type: 'REMOVE',
payload: { id },
});
return <Item {...props} remove={remove} />;
});
const initialState = [{ id: 1 }, { id: 2 }, { id: 3 }];
//Reducer is static it doesn't need list to be in it's
// scope through closure
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
//remove the id from the list
return state.filter(
item => item.id !== action.payload.id
);
}
return state;
};
export default () => {
//initialize state and reducer
const [list, dispatch] = useReducer(
reducer,
initialState
);
console.log('parent render', list);
return (
<div>
{list.map(({ id }) => (
<ItemContainer
key={id}
id={id}
dispatch={dispatch}
/>
))}
</div>
);
};
I created useBanner hooks
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState(array.slice(0, yardage));
const [bannListIndex, setBannIndex] = useState(1);
return {
....
};
};
Am I doing the right thing and the props throw in useState.
It’s permissible to use useBanner.
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
when props will change here.
Will change the state in useBanner.
or is it considered anti-patterns I have to write all this in useMemo
const useBanner = (array, yardage) => {
const [bannArr, setBannArr] = useState([]);
const [bannListIndex, setBannIndex] = useState(1);
useMemo(() => {
setBannArr(array.slice(0, yardage));
setBannIndex(1);
}, [array, yardage]);
return {
....
};
};
Yes, custom hooks are possible in React. Here is separate document discussing custom hooks.
But exactly you sample may require additional code depending on what is your final goal.
If you want initialize state only once, when component Banner is first created, you can just do as in your first sample
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr } = useBanner(array, yardage);
return (
...
);
};
This will work perfectly. But if props array and yardage will change, this will not be reflected in component. So props will be used only once as initial values and then will not be used in useBanner even if changed (And it doesn't matter whether you'll use useBanner or useState directly). This answer highlight this.
If you want to update inital values on each props change, you can go with useEffect like below
const Banner= ({
array,
yardage
}) => {
const { bannForth, bannBeck, bannArr, setBannArr } = useBanner(array, yardage);
useEffect (() => {
// setBannArr should also be returned from useBanner. Or bannArr should be changed with any other suitable function returned from useBanner.
setBannArr(array.slice(0, yardage));
}, [array, yardage, setBannArr])
return (
...
);
};
In this case Banner component can control state itself and when parent component change props, state in Banner component will be reset to new props.
Here is small sample to showcase second option.
When passing callback to component, I should use useCallback hook to return a memoized callback (to prevent unneeded renders):
import doSomething from "./doSomething";
const FrequentlyRerenders = ({ id }) => {
const onEvent = useCallback(() => doSomething(id), [id]);
return (
<ExpensiveComponent onEvent={ onEvent } />
);
};
But what if I am using map? for example:
import doSomething from "./doSomething";
const FrequentlyRerendersMap = ({ ids }) => {
return ids.map(id => {
const onEvent = useCallback(() => doSomething(id), [id]);
return (
<ExpensiveComponent key={id} onEvent={ onEvent } />
);
});
};
How should I properly use useCallback? Is the above the right way to pass multiple callbacks? Is it really works and know to memioze every callback according to an item of an array?
Convert the returned mapped JSX into a component and then you can useCallback without problems
import doSomething from "./doSomething";
const MappedComponent =(props) => {
const onEvent = useCallback(() => doSomething(props.id), []);
return (
<ExpensiveComponent onEvent={ onEvent } />
);
}
const FrequentlyRerendersMap = ({ ids }) => {
return ids.map(id => {
return <MappedComponent key={id} id={id} />
});
};
This is now expressly discouraged in the docs.
https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
Old answer
The answer to re architect is sidestepping the question IMO. Though, I think creating a new component is probably a good idea.
To answer the question though, your code:
import doSomething from "./doSomething";
const FrequentlyRerendersMap = ({ ids }) => {
return ids.map(id => {
const onEvent = useCallback(() => doSomething(id), [id]);
return (
<ExpensiveComponent key={id} onEvent={ onEvent } />
);
});
};
Is actually what you would want to do to memoize in a map. I don't know the implementation of useCallback, but it should add very little memory overhead. A stackFrame, and whatever they do to reduce the array into some kind of key for the memoized function.
Unless your working on some with a TON of elements, EG react virtualized components unlimited scrolling, you are realistically safe to useCallback the way you are. In fact, the small memory overhead is probably a much cheaper price than the rerender of all of those components.