I'm using custom hooks for a component, and the custom hook uses a custom context. Consider
/* assume FooContext has { state: FooState, dispatch: () => any } */
const useFoo = () => {
const { state, dispatch } = useContext(FooContextContext)
return {apiCallable : () => apiCall(state) }
}
const Foo = () => {
const { apiCallable } = useFoo()
return (
<Button onClick={apiCallable}/>
)
}
Lots of components will be making changes to FooState from other components (form inputs, etc.). It looks to me like Foo uses useFoo, which uses state from FooStateContext. Does this mean every change to FooContext will re-render the Foo component? It only needs to make use of state when someone clicks the button but never otherwise. Seems wasteful.
I was thinking useCallback is specifically for this, so I am thinking return {apiCallable : useCallback(() => apiCall(state)) } but then I need to add [state] as a second param of useCallback. Then that means the callback will be re-rendered whenever state updates, so I'm back at the same issue, right?
This is my first time doing custom hooks like this. Having real difficulty understanding useCallback. How do I accomplish what I want?
Edit Put another way, I have lots of components that will dispatch small changes to deeply nested properties of this state, but this particular component must send the entire state object via a RESTful API, but otherwise will never use the state. It's irrelevant for rendering this component completely. I want to make it so this component never renders even when I'm making changes constantly to the state via keypresses on inputs (for example).
Since you provided Typescript types in your question, I will use them in my response.
Way One: Split Your Context
Given a context of the following type:
type ItemContext = {
items: Item[];
addItem: (item: Item) => void;
removeItem: (index: number) => void;
}
You could split the context into two separate contexts with the following types:
type ItemContext = Item[];
type ItemActionContext = {
addItem: (item: Item) => void;
removeItem: (index: number) => void;
}
The providing component would then handle the interaction between these two contexts:
const ItemContextProvider = () => {
const [items, setItems] = useState([]);
const actions = useMemo(() => {
return {
addItem: (item: Item) => {
setItems(currentItems => [...currentItems, item]);
},
removeItem: (index: number) => {
setItems(currentItems => currentItems.filter((item, i) => index === i));
}
};
}, [setItems]);
return (
<ItemActionContext.Provider value={actions}>
<ItemContext.Provider value={items}>
{children}
</ItemContext.Provider>
</ItemActionContext.Provider>
)
};
This would allow you to get access to two different contexts that are part of one larger combined context.
The base ItemContext would update as items are added and removed causing rerenders for anything that was consuming it.
The assoicated ItemActionContext would never update (setState functions do not change for their lifetime) and would never directly cause a rerender for a consuming component.
Way Two: Some Version of an Subscription Based Value
If you make the value of your context never change (mutate instead of replace, HAS THE WORLD GONE CRAZY?!) you can set up a simple object that holds the data you need access to and minimises rerenders, kind of like a poor mans Redux (maybe it's just time to use Redux?).
If you make a class similar to the following:
type Subscription<T> = (val: T) => void;
type Unsubscribe = () => void;
class SubscribableValue<T> {
private subscriptions: Subscription<T>[] = [];
private value: T;
constructor(val: T) {
this.value = val;
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.subscribe = this.subscribe.bind(this);
}
public get(): T {
return this._val;
}
public set(val: T) {
if (this.value !== val) {
this.value = val;
this.subscriptions.forEach(s => {
s(val)
});
}
}
public subscribe(subscription: Subscription<T>): Unsubscriber {
this.subscriptions.push(subscription);
return () => {
this.subscriptions = this.subscriptions.filter(s => s !== subscription);
};
}
}
A context of the following type could then be created:
type ItemContext = SubscribableValue<Item[]>;
The providing component would look something similar to:
const ItemContextProvider = () => {
const subscribableValue = useMemo(() => new SubscribableValue<Item[]>([]), []);
return (
<ItemContext.Provider value={subscribableValue}>
{children}
</ItemContext.Provider>
)
};
You could then use some a custom hooks to access the value as needed:
// Get access to actions to add or remove an item.
const useItemContextActions = () => {
const subscribableValue = useContext(ItemContext);
const addItem = (item: Item) => subscribableValue.set([...subscribableValue.get(), item]);
const removeItem = (index: number) => subscribableValue.set(subscribableValue.get().filter((item, i) => i === index));
return {
addItem,
removeItem
}
}
type Selector = (items: Item[]) => any;
// get access to data stored in the subscribable value.
// can provide a selector which will check if the value has change each "set"
// action before updating the state.
const useItemContextValue = (selector: Selector) => {
const subscribableValue = useContext(ItemContext);
const selectorRef = useRef(selector ?? (items: Item[]) => items)
const [value, setValue] = useState(selectorRef.current(subscribableValue.get()));
const useEffect(() => {
const unsubscribe = subscribableValue.subscribe(items => {
const newValue = selectorRef.current(items);
if (newValue !== value) {
setValue(newValue);
}
})
return () => {
unsubscribe();
};
}, [value, selectorRef, setValue]);
return value;
}
This would allow you to reduce rerenders using selector functions (like an extremely basic version of React Redux's useSelector) as the subscribable value (root object) would never change reference for its lifetime.
The downside of this is that you have to manage the subscriptions and always use the set function to update the held value to ensure that the subscriptions will be notified.
Conclusion:
There are probably a number of other ways that different people would attack this problem and you will have to find one that suits your exact issue.
There are third party libraries (like Redux) that could also help you with this if your context / state requirements have a larger scope.
Does this mean every change to FooContext will re-render the Foo component?
Currently (v17), there is no bailout for Context API. Check my another answer for examples. So yes, it will always rerender on context change.
It only needs to make use of state when someone clicks the button but never otherwise. Seems wasteful.
Can be fixed by splitting context providers, see the same answer above for explanation.
Related
This hook create an inifite loop. I don't understnd why, since my dependencies array is set.
Error : Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Custom hook :
export const useListGuessers = () => {
const [list, setList] = useState([]);
const hasMarketing = UserHelper.hasAuthorization(AUTHORIZATION_MARKETING);
const hasTechnical = UserHelper.hasAuthorization(AUTHORIZATION_TECHNICAL);
const dashboardGroups = new DashboardGroups({hasMarketing, hasTechnical});
const guessers = [
...dashboardGroups.appProductGroup(),
...dashboardGroups.articlesGroup(),
...dashboardGroups.mediasGroup(),
...dashboardGroups.productsGroup(),
...dashboardGroups.orderableProductsGroup(),
...dashboardGroups.typesGroup(),
...dashboardGroups.usersGroup(),
...dashboardGroups.othersGroup(),
...dashboardGroups.userManagementGroup(),
];
const filteredGuesser = guessers
.filter(({canShow}) => canShow)
.map((guesser: any) => {
return {
label: guesser.label ?? guesser.value.options.label,
link: (guesser.operation && `user-management/${guesser.operation}`) ?? guesser.value.name,
};
})
.sort((a, b) => a.label.localeCompare(b.label));
useEffect(() => {
filteredGuesser && setList(filteredGuesser);
}, [filteredGuesser]);
return list;
};
The class :
export class DashboardGroups {
authorizations: {hasMarketing: boolean; hasTechnical: boolean};
constructor(authorizations: {hasMarketing: boolean; hasTechnical: boolean}) {
this.authorizations = authorizations;
}
// [all groups comes here...]
getGroups = () => {
// return an object for each groups with labels, and the group as "children"
};
}
Since filteredGuesser calculates on each re-render, which triggers useEffect(..., [filterGuesser] which causes re-render... so it loops.
The easiest straighforward solution is to ensure reference equality for filteredGuesser with useMemo. Then it will be referentially the same until guessers is changed:
const filteredGuesser = useMemo(() =>
guessers
.filter(({canShow}) => canShow)
.map((guesser: any) => {
label: guesser.label ?? guesser.value.options.label,
link: (guesser.operation && `user-management/${guesser.operation}`) ?? guesser.value.name,
})
.sort((a, b) => a.label.localeCompare(b.label))
, [guessers]);
However, I think the better solution would be reconsider need in
useEffect(() =>
...
setList(filteredGuesser)
This storing ready for use calculation into state does not seem reasonable to me. I think you better use filteredGuesser directly, instead of storing it into list state
Beta docs for useMemo
Referential equality aka strict equality on MDN
I am aware of the multiple similar questions regarding this type of error both on articles and on SO as well but none of them have applied or worked for me up until this point.
So, I am using the React Context API to keep track of a state shared between multiple components:
// imports removed because irrelevant
interface IMarkedPlacesContext {
markedPlaces: Place[]
setMarkedPlaces: React.Dispatch<React.SetStateAction<Place[]>>
markPlaces: (places : Place[]) => Promise<void>
}
interface Props {
children: React.ReactNode
}
const MarkedPlacesContext = createContext<IMarkedPlacesContext>(
{} as IMarkedPlacesContext
)
export function MarkedPlacesContextProvider({ children }: Props) {
const [markedPlaces, setMarkedPlaces] = useState<Place[]>([])
const markPlaces = useCallback( async (places: Place[]) => {
const placesWithRatings : Place[] = await Promise.all(places.map(async (place) => {
const placeRating = await FirestorePlaceRatingService.getPlaceRating(place.placeId)
if (placeRating) {
return({
...place,
rating: placeRating.sumRating/placeRating.reviewCount,
reviewCount: placeRating.reviewCount
})
} else {
return(place)
}
}))
setMarkedPlaces(placesWithRatings)
}, [])
return (
<MarkedPlacesContext.Provider
value={{
markedPlaces,
setMarkedPlaces,
markPlaces,
}}
>
{children}
</MarkedPlacesContext.Provider>
)
}
export function useMarkedPlaces() {
return useContext(MarkedPlacesContext)
}
This component sits on the root level of my app. The function to take note of here is markPlaces which fetches the corresponding rating (if it exists) from my Firestore backend, otherwise, it just gives back the original place object with rating as undefined.
Now the problem occurs when I try to use markPlaces in a useEffect in one of my components:
const PlaceListScreen = () => {
const { markedPlaces, markPlaces, setMarkedPlaces } = useMarkedPlaces()
const { setPlaceInfo, focusPlace } = usePlace()
const { navigate } =
useNavigation<StackNavigationProp<BottomSheetStackParams>>()
useEffect(() => {
if (focusPlace) navigate('PlaceReview')
}, [focusPlace])
// INFINITE LOOP OCCURS HERE! :(
useEffect(() => {
console.log('marking..')
markPlaces(markedPlaces)
}, [])
const handlePressPlace = (place: Place) => {
setPlaceInfo(place)
navigate('PlaceReview')
}
const renderRating = (placeRating: number | undefined) => (
<Subheading style={{flex: 0.1, paddingLeft: 15}}>
{placeRating ? placeRating.toFixed(1) : '-' }
</Subheading>
)
const renderPlaceItem = (place: Place) => (
<TouchableOpacity onPress={() => handlePressPlace(place)}>
<List.Item
title={place.name}
description={place.formattedAddress}
right={() => renderRating(place.rating)}
/>
</TouchableOpacity>
)
return (
<BottomSheetFlatList
data={markedPlaces}
keyExtractor={(place) => place.placeId}
renderItem={(place) => renderPlaceItem(place.item)}
/>
)
}
export default PlaceListScreen
I do not understand why calling markPlaces in this manner causes an infinite loop. Is it something to do with the way I pass in markedPlaces as a param? Or is it something to do with the way I have structured my context?
Any help would be much appreciated :)
UPDATE
I have a hacky work-around of the issue, what I did was to make a local state called places held on the component PlaceListScreen which essentially acts as a reflection of the main value store in the MarkedRestaurants Context. Instead of changing the state held within the context, I change the local state. However, this is an extremely ugly solution which I could see definitely causing problems in the future.
I've been mulling over why having the value stored in context is the cause of the infinite re-renders, but I'm still failing to see why. If someone could help me figure out why this is happening and a more appropriate solution for it that would be awesome.
I am passing a state prop from parent to child:
Parent:
const [refresh, setRefresh] = React.useState<boolean>(true);
passing into Component as
<Chart setRefresh={setRefresh}/>
Child
interface IChart {
refreshChart?: boolean;
setRefresh?: (newRefresh: boolean) => void;
}
const Chart: React.FunctionComponent<IChart> = ({refreshChart, setRefresh}) => {
const handleRefresh = () => {
setRefresh(false);
}
}
Obviously it's to do with the type in the IChart interface as if I use any or not make it optional it works, but I need it optional and want to avoid any.
Obviously it's to do with the type in the IChart interface as if I use any or not make it optional it works, but I need it optional and want to avoid any.
If you want to keep the property optional, then you have to deal with the fact it's optional, either by making the call to it check first:
const Chart: React.FunctionComponent<IChart> = ({refreshChart, setRefresh}) => {
const handleRefresh = () => {
if (setRefresh) { // ***
setRefresh(false);
}
};
}
...or by having a default:
const Chart: React.FunctionComponent<IChart> = ({refreshChart, setRefresh = (b: boolean) => {}}) => {
// ^^^^^^^^^^^^^^^^^^^^^
const handleRefresh = () => {
setRefresh(false);
};
}
You might even make creating handleRefresh conditional based on whether setRefresh exists, and possibly even change what the component renders (e.g., no refresh UI) if setRefresh isn't provided.
it's because in IChart interface you're saying that this property is optional, so it is "possibly undefined". Just remove "?" from setRefresh if the value is required.
I do with Similar cases a simple if condition basically because if tweak it a teammate may come in the future and integrate it wrongly, or add a default value to it
const handleRefresh = () => {
if(setRefresh)
setRefresh(false);
}
I created a hook to use a confirm dialog, this hook provides the properties to the component to use them like this:
const { setIsDialogOpen, dialogProps } = useConfirmDialog({
title: "Are you sure you want to delete this group?",
text: "This process is not reversible.",
buttons: {
confirm: {
onPress: onDeleteGroup,
},
},
width: "360px",
});
<ConfirmDialog {...dialogProps} />
This works fine, but also I want to give the option to change these properties whenever is needed without declaring extra states in the component where is used and in order to achieve this what I did was to save these properties in a state inside the hook and this way provide another function to change them if needed before showing the dialog:
interface IState {
isDialogOpen: boolean;
dialogProps: TDialogProps;
}
export const useConfirmDialog = (props?: TDialogProps) => {
const [state, setState] = useState<IState>({
isDialogOpen: false,
dialogProps: {
...props,
},
});
const setIsDialogOpen = (isOpen = true) => {
setState((prevState) => ({
...prevState,
isDialogOpen: isOpen,
}));
};
// Change dialog props optionally before showing it
const showConfirmDialog = (dialogProps?: TDialogProps) => {
if (dialogProps) {
const updatedProps = { ...state.dialogProps, ...dialogProps };
setState((prevState) => ({
...prevState,
dialogProps: updatedProps,
}));
}
setIsDialogOpen(true);
};
return {
setIsDialogOpen,
showConfirmDialog,
dialogProps: {
isOpen: state.isDialogOpen,
onClose: () => setIsDialogOpen(false),
...state.dialogProps,
},
};
};
But the problem here is the following:
Arguments are passed by reference so if I pass a function to the button (i.e onDeleteGroup) i will keep the function updated to its latest state to perform the correct deletion if a group id changes inside of it.
But as I'm saving the properties inside a state the reference is lost and now I only have the function with the state which it was declared at the beginning.
I tried to add an useEffect to update the hook state when arguments change but this is causing an infinite re render:
useEffect(() => {
setState((prevState) => ({
...prevState,
dialogProps: props || {},
}));
}, [props]);
I know I can call showConfirmDialog and pass the function to update the state with the latest function state but I'm looking for a way to just call the hook, declare the props and not touch the dialog props if isn't needed.
Any answer is welcome, thank you for reading.
You should really consider not doing this, this is not a good coding pattern, this unnecessarily complicates your hook and can cause hard to debug problems. Also this goes against the "single source of truth" principle. I mean a situation like the following
const Component = ({title}: {title?: string}) => {
const {showConfirmDialog} = useConfirmDialog({
title,
// ...
})
useEffect(() => {
// Here you expect the title to be "title"
if(something) showConfirmDialog()
}, [])
useEffect(() => {
// Here you expect the title to be "Foo bar?"
if(somethingElse) showConfirmDialog({title: 'Foo bar?'})
}, [])
// But if the second dialog is opened, then the first, the title will be
// "Foo bar?" in both cases
}
So please think twice before implementing this, sometimes it's better to write a little more code but it will save you a lot debugging.
As for the answer, I would store the props in a ref and update them on every render somehow like this
/** Assign properties from obj2 to obj1 that are not already equal */
const assignChanged = <T extends Record<string, unknown>>(obj1: T, obj2: Partial<T>, deleteExcess = true): T => {
if(obj1 === obj2) return obj1
const result = {...obj1}
Object.keys(obj2).forEach(key => {
if(obj1[key] !== obj2[key]) {
result[key] = obj2[key]
}
})
if(deleteExcess) {
// Remove properties that are not present on obj2 but present on obj1
Object.keys(obj1).forEach(key => {
if(!obj2.hasOwnProperty(key)) delete result[key]
})
}
return result
}
const useConfirmDialog = (props) => {
const localProps = useRef(props)
localProps.current = assignChanged(localProps.current, props)
const showConfirmDialog = (changedProps?: Partial<TDialogProps>) => {
localProps.current = assignChanged(localProps.current, changedProps, false)
// ...
}
// ...
}
This is in case you have some optional properties in TDialogProps and you want to accept Partial properties in showConfirmDialog. If this is not the case, you could simplify the logic a little by removing this deleteExcess part.
You see that it greatly complicates your code, and adds a performance overhead (although it's insignificant, considering you only have 4-5 fields in your dialog props), so I really recommend against doing this and just letting the caller of useConfirmDialog have its own state that it can change. Or maybe you could remove props from useConfirmDialog in the first place and force the user to always pass them to showConfirmDialog, although in this case this hook becomes kinda useless. Maybe you don't need this hook at all, if it only contains the logic that you have actually shown in the answer? It seems like pretty much the only thing it does is setting isDialogOpen to true/false. Whatever, it's your choice, but I think it's not the best idea
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.