I have a react functional component that accesses the MobX store with useContext. I have found two ways to observe an array that is an observable from the store. First, the useObserver hook and wrapping the component with observer.
I thought that these are the same but that the useObserver only observes specific properties (such as the array that is passed) but I am experiencing a problem when the array reaches size 2 and then the component does not re-render. That's the case when using useObserver. When wrapping with observer, this is fixed.
Can anyone explain why this is happening and what's the difference?
const ApplesContainer = observer(() => {
const stores = useStores();
const applesArray = stores.fruits.apples;
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
// OR with useObserver()
function useGlobalState() {
const stores = useStores();
return useObserver(() => ({
applesArray: stores.fruits.apples
}));
}
const ApplesContainer = observer(() => {
const { applesArray } = useGlobalState();
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
useObserver must return JSX with an observable value.
This hook takes care of tracking changes and re-rendering them.
If no observable value exists in JSX, then it won't be re-rendered.
e.g.:
const SomeContainer =() => {
const { someStores } = useStores();
return useObserver(()=>(
{someStore.data.map(val => (
<Apple key={val.id} val={val} />
))}
));
};
Related
Here is the code , no idea why Mem re-render after set state, as it is a memoized component, or if i wanna remember the component with set state, i should use useRef? that stupid??
const Demo = () => {
console.log("render")
const data = LoadSomeData();
return (<>{data.id}</>)
}
const Mycomp = ({...props}) => {
const [showSearch, setShowSearch] = useState(false);
const Mem = useMemo(() => <Demo />, [props.iwandToReloadData]);
return (
<>
{ showSearch ?
<button onClick={()=>setShowSearch(false)}>Back</button>
:
<>
{Mem}
<button onClick={()=>setShowSearch(true)}>Search</button>
</>
}
</>
)
}
export default Mycomp;
Refer to the comment from Tony Nguyen, it is because i use conditional render for the memorised component, thus it will trigger unmount of the memorised component. Therefore nothing can be memorised.
the solution is using css to hide it instead of not render it for my case
I am trying different patterns on a new React application, In this case I have a redux store which contains a projects array of objects.
When the component loads I dispatch a new action which gets all the projects and set the store with the new values, in this case the list is rendered correctly.
When I dispatch the same actions from a children of the same component, the redux state is updated, the main component is rerendered but the list remain the same.
export const ProjectList: React.FC = () => {
//When projects change the component should rerender with the new store
const { projects, isLoading, errors } = useSelector((store: IAppState) => store.projectState);
const dispatch = useDispatch();
const { setActiveProject } = useActiveProjectValue();
const { setIsNewTask } = useIsNewTaskValue();
useEffect(() => {
console.log('called');
dispatch(getProjectsAction());
}, []);
return (
<div className="ProjectList">
{!isLoading ? (
projects.map((project: IProject) => (
<ProjectListItem
key={project.docId ? project.projectId : uuid()}
handleClick={() => handleClick(project)}
project={project}
>
{project.name}
</ProjectListItem>
))
) : (
<p>Loading projects</p>
)}
{errors && <p>{errors}</p>}
</div>
);
};
I have also tried to change useEffect with useMemo and add a project dependency to this function, in this case it works but it seems to trigger the action too many times:
useEffect(() => {
dispatch(getProjectsAction());
}, [projects]);
Any advice/solution?
Thanks!
Every time I use react and the useEffect method my state variable renders twice. Once an empty variable and the next the desired variable. What can I try to help avoid this problem for now and in the future?
import React, { useState,useEffect } from "react";
export default function Member (props) {
const [team,setTeam] = useState([]);
useEffect(() => {
let array = ["hello","hi"];
setTeam(array);
}, [])
console.log(team);
return (
<>
{team.forEach(i => <p>{i}</p>)}
</>
)
}
You need to use map to render an array in JSX:
export default function Member(props) {
const [team, setTeam] = useState([]);
useEffect(() => {
let array = ["hello", "hi"];
setTeam(array);
}, []);
console.log(team);
return (
<>
{team.map(i => ( // use map here
<p>{i}</p>
))}
</>
);
}
forEach doesn't return anything, so you can't use it to render components like that.
Also in your code instead of using useEffect to setup initial state, you can just set it straight in useState:
export default function Member(props) {
const [team, setTeam] = useState(["hello", "hi"]);
console.log(team);
return (
<>
{team.map(i => ( // use map here
<p>{i}</p>
))}
</>
);
}
It is an abvious behavior.
Component render's first time with initial state. After useEffect (componentDidMount) again re-renders.
So you are getting two console.log.
To avoid this, you need to set the state initally,
const [team,setTeam] = useState(["hello","hi"]);
and remove useEffect.
Note: forEach won't print data, you need map here,
{team.map(i => <p key={i}>{i}</p>)} //provide a key here
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>
);
};
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.