Best way to handle button's onPress from anywhere - reactjs

The idea
I have a screen, which has a 'global' button on top, and some inputs on the bottom.
By default this 'global' button will go to 'home' screen.
I want that button to work like this:
[input focused] -> [button changes to 'back'] (when button is in 'back' state it will blur the input on press)
[button clicked] -> [input blurred] -> [button changes back to 'home'].
So I need to figure out how to handle button's onPress in any component in the app.
(With inputs I could do it with only Keyboard.dismiss(), but it's not only about inputs, I have some components that have their own way to do something on button's press)
My approach
I've already made this working with context api, but right now I don't know was it a good idea or not...
I have a context provider that looks like this:
export default function MainButtonControlProvider({ children, goToHome }) {
const [_type, setType] = useState('home');
const [_onPressFn, setOnPressFn] = useState(() => goToHome);
const reset = () => {
setType('home');
setOnPressFn(() => goToHome);
}
const set = ({ type, onPress }) => {
setType(type);
setOnPressFn(() => onPress);
}
return (
<MainButtonControlContext.Provider value={{ _type, _onPressFn, reset, set }}>
{children}
</MainButtonControlContext.Provider>
)
}
And that 'global' button like this:
export default function MainButton() {
const { _type, _onPressFn } = useContext(MainButtonControlContext);
const getIcon = () => {
switch (_type) {
case 'home':
return <HomeIcon />
case 'back':
return <ChevronIcon size={32} rotation={-90} />
}
}
return (
<Button icon={getIcon()} onPress={_onPressFn} />
)
}
And input like this:
export default function BasicInput() {
const inputRef = useRef(null);
const { set, reset } = useContext(MainButtonControlContext);
return (
<TextInput
ref={inputRef}
onFocus={() => set({ type: 'back', onPress: () => inputRef.current.blur() })
onBlur={() => reset()}
/>
)
}
The problem
So the input is focused, it changes 'global' button's look and onPress function.
The problem is that after set() or reset() are executed, every other component in the tree that also uses these set() and reset() functions from the MainButtonControlContext - rerender...
It's pretty logical that they do this, because state is changed and functions are remade in the context provider... But how do I get around this?
I could've used useCallback() on the set() and reset() functions, but set() takes arguments and they are always different, so there's no point in that.
So I need a different approach 🥲
I have never used redux, but I think it is a good idea to learn it, so if someone has an approach with it, I wouldn't mind changing the whole app for PERFORMANCE 😅
The thing that I didn't understand is how to handle button's onPress for that button, since redux state needs to be serializable...
Edit:
I tried to replace set() function in context provider with just set from useState, and wrap reset() with useCallback, and it worked! No more rerenders, but it's a pain to set all of the props when changing the button (I have 2 more states now). If someone knows how to change the set function from useState a bit without triggering rerenders, pls help ;)
I also tried to replace all of the useStates with useReducer, but for some reason change in state from useReducer won't trigger button's rerender...

The approach with Context API didn't work. I fixed rerenders of the components that used context by wrapping set and reset function in useCallback(I didn't know that I can pass arguments to useCallback 😅), but it did nothing because ALL of the components inside provider still rerendered after state change... I tried even memoizing the state, still didn't work.
I think I am missing something with using context api. If someone knows how to deal with state inside Providers, let me know, will be a good lesson ;)
I ended up using zustand to store the state needed for the main button. Redux felt like overkill for such a simple app and also I don't have any experience with it.
So I just created a store for the main button, like this:
import create from 'zustand';
export const useMainButtonStore = create((set, get) => ({
type: 'home',
onPress: () => console.warn('No onPress set in MainButttonStore.'),
_default: null,
set: (payload) => set(state => ({
/* state from payload */
})),
reset: () => set(state => ({
/* check if _default is set and then set state to _default */
})),
setDefault: (payload) => set(state => ({
/* set _default on initial app render */
})),
}))
And then in components.
These 2 can actually be combined in one line using shallow.
const setMainButton = useMainButtonStore(state => state.set);
const resetMainButton = useMainButtonStore(state => state.reset);
One concern that I had is whether it is ok to store functions. Up to this point I had no problems with this.
I'm sure there will be problems when persisting this store, because functions are not serializable, but there's no point in that :)

Related

How does one cache an entire functional component in React?

I've built a tabs component. Each tab, when clicked, changes the contents of the "main screen". Assume a tab had some back-end call to retrieve data it needs to render, then it makes no sense to have it re-run these calls every time the user clicks another tab and comes back to it. I want to retrieve what was rendered before and display it.
I looked into memo, the big warning says to "not rely on it to “prevent” a render, as this can lead to bugs.", nor does it work. Every time I wrap my component in a memo, the test:
useEffect(() => {
console.log('Rendered');
}, [])
Still runs, telling me that the component re-rendered. Then I thought about memoizing the return itself, so, like:
export const MyComponent = (context) => {
const content = useMemo(() => {
return <></>
}, [context]);
return content;
};
But quickly realized that by the time I reach this useMemo, I'm already in the re-rendering cycle, because there's no way for React to know that MyComponent's useMemo existed in the past, so, again, it re-renders the whole thing. This, in turn made me think that the memoization needs to be done at the level where MyComponent is being rendered, not inside of it but I don't know how to do it.
How can I skip re-renders if my props haven't changed?
Read all the articles, tried all the things but to no avail.
Concisely, here is my component and my latest approach:
export const MyComponent = memo(({ context, className = '', ...props }) => {
..
..
});
The interesting bit here is context. This should almost never change. Its structure is a deeply nested object, however, when I play with memo's second argument, its diff function, what ends up happening if I put a console.log in there, as follows:
const MyComponent = ({ context, className = '', ...props }) => {
};
const areEqual = (prevProps, nextProps) => {
console.log('Did equality check.');
};
export default memo(MyComponent, areEqual);
I will only see "Did equality check." once. No matter what I do, I can't seem to get a memoized component out of memo. This is how MyComponent's parent looks like:
const Parent = ({}) => {
const context = useSelector(); //context comes from the store.
const [selectedTab, setSelectedTab] = useState(false);
const [content, setContent] = useState(null);
useEffect(() => {
switch (selectedTab) {
case 'components':
setContent(<MyComponent context={context} />);
break;
}
}, [selectedTab, context]);
return(<>{content}</>);
};
#daniel-james I think the problem here is we are un-mounting the whole component when we are switching tabs. Instead try memoizing the component in the parent itself. That way your component is memoized only once.

How can I prevent unnecessary re-renders on the child components in React with Hooks & Context?

I'm working on some code, and this code is huge. We have so many child components(nearly 300) and each one of them are using & manipulating values from the parent component's state via React Context.
Note: I didn't wrote this code from scratch. Which also means the design I'm about to show is not what I would come up with.
The problem: Since every component is using the same state, there are so many unnecessary re-renders happening. Every small state change is causing every component to re-render. And it makes the web app laggy. Literally, there is lag when you enter some input in a field.
I think, when state change happens the functions get rebuilt and that's why every child gets updated, because the value provided by context is changed after state change happened.
Additionally, I tried to use useReducer instead of useState that didn't went well. Besides that, I tried to use React.memo on every child component but the compare function didn't get triggered no matter what I tried. compare function only got triggered on the parent component which has the state as props. At this point, I'm not even really sure what is the problem :D
To give more specific details on the design, here is how we define and pass the callbacks to child components.
Definitions:
const [formItemState, setFormState] = React.useState<FormItemState>({} as FormItemState);
const getAppState = useCallback(() => ({ state: props.state as AppState }), [props.state]);
const getAppAction = useCallback(() => ({ action: props.action as AppAction }), [props.action]);
const getFormItemError = useCallback((key: string) => formItemErrorState[key], [
formItemErrorState,
]);
const getFormItem = useCallback((key: string) => formItemState[key], [formItemState]);
const updateFormItem = useCallback(
(name: string, formItemData: FormItemData): void => {
const previousState = getFormItem(name);
if (!isEqual(previousState, formItemData)) {
formItemState[name] = formItemData;
setFormState((state) => ({
...state,
...formItemState,
}));
}
},
[formItemState, setFormState]
);
Passing them to Context.Provider:
return (
<FormContext.Provider
value={{
getAppAction,
getAppState,
getFormItem,
updateFormItem
}}
>
<SomeComponent>
{props.children} // This children contains more than 250 nested components, and each one of them are using these provided functions to interact with the state.
</SomeComponent>
</FormContext.Provider>
);
Last note: Please ask me if more info is needed. Thanks!
Why dont you just rewrite your state management to redux and pull only the necessary state to be used on each component. React.memo only picks up changes from props

Prevent useState value from being reset when props change

I have a component that looks something like this:
//#flow
import React, { useState } from "react";
type Props = {
likes: int,
toggleLike: () => void,
};
const Foo = (props: Props) => {
const [open, setOpen] = useState(false);
const style = `item${open ? " open": ""}`;
return (
<div className={style} onMouseOver={() => setOpen(true)} onFocus={() => setOpen(true)} onMouseOut={() => setOpen(false)} onBlur={() => setOpen(false)}>
<button onClick={props.toggleLike}>Toggle like</button>
</div>
);
};
export default Foo;
The open state is used to apply the "open" class when moused over. The problem comes if I call the toggleLike() prop function, since this updates the props and the component is rerendered with open reset to false. As the style uses a transition, this results in the animation rerunning as it changes back to false, then to true due to the mouse being over it.
So, how can I prevent open being reset back to false on each subsequent render? It seems like it should be straightforward, but after going through https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state I can't seem to apply it in my case.
State does not reset when props change. State is on a per component basis and is preserved throughout re-renders, hence being called "state".
As Dennis Vash already mentioned, the problem is most likely caused by the component being unmounted or replaced by an identical component. You can verify this easily by adding this to your component:
useEffect(() => {
console.log("Mounted")
}, [])
You should see multiple "Mounted" in the console.
If there's no way to prevent the component from being replaced or unmounted, consider putting the state into a context and consume that context inside your component, as you can also wrap each of your components into its own context to give it a unique, non-global, state.

What is the 'proper' way to update a react component after an interval with hooks?

I'm using the alpha version of react supporting hooks, and want to validate my approach to updating the text in a component after an interval without rendering the component more times than needed when a prop changes.
EDIT: For clarity - this component is calling moment(timepoint).fromNow() within the formatTimeString function (docs here), so the update isn't totally unneccessary, I promise!
I previously had:
const FromNowString = ({ timePoint, ...rest }) => {
const [text, setText] = useState(formatTimeString(timePoint));
useEffect(() => {
setText(formatTimeString(timePoint));
let updateInterval = setInterval(
() => setText(formatTimeString(timePoint)),
30000
);
return () => {
clearInterval(updateInterval);
};
}, [timePoint]);
// Note the console log here is so we can see when renders occur
return (
<StyledText tagName="span" {...rest}>
{console.log('render') || text}
</StyledText>
);
};
This "works" - the component correctly updates if the props change, and the component updates at each interval, however on mounting, and when a prop changes, the component will render twice.
This is because useEffect runs after the render that results when the value of timePoint changes, and inside my useEffect callback I'm immediately calling a setState method which triggers an additional render.
Obviously if I remove that call to setText, the component doesn't appear to change when the prop changes (until the interval runs) because text is still the same.
I finally realised I could trigger a render by setting a state variable that I didn't actually need, like so:
const FromNowString = ({ timePoint, ...rest }) => {
// We never actually use this state value
const [, triggerRender] = useState(null);
useEffect(() => {
let updateInterval = setInterval(() => triggerRender(), 30000);
return () => {
clearInterval(updateInterval);
};
}, [timePoint]);
return (
<StyledText tagName="span" {...rest}>
{console.log("render") || formatTimeString(timePoint)}
</StyledText>
);
};
This works perfectly, the component only renders once when it mounts, and once whenever the timePoint prop changes, but it feels hacky. Is this the right way of going about things, or is there something I'm missing?
I think this approach seems fine. The main change I would make is to actually change the value each time, so that it is instead:
const FromNowString = ({ timePoint, ...rest }) => {
const [, triggerRender] = useState(0);
useEffect(() => {
const updateInterval = setInterval(() => triggerRender(prevTriggerIndex => prevTriggerIndex + 1), 30000);
return () => {
clearInterval(updateInterval);
};
}, [timePoint]);
return (
<StyledText tagName="span" {...rest}>
{console.log("render") || formatTimeString(timePoint)}
</StyledText>
);
};
I have two reasons for suggesting this change:
I think it will help when debugging and/or verifying the exact behavior that is occurring. You can then look at this state in dev tools and see exactly how many times you have triggered the re-render in this manner.
The other reason is just to give people looking at this code more confidence that it will actually do what it is intended to do. Even though setState reliably triggers a re-render (and React is unlikely to change this since it would break too much), it would be reasonable for someone looking at this code to wonder "Does React guarantee a re-render if a setState call doesn't result in any change to the state?" The main reason setState always triggers a re-render even if unchanged is because of the possibility of calling setState after having done mutations to the existing state, but if the existing state is null and nothing is passed in to the setter, that would be a case where React could know that state has not changed since the last render and optimize for it. Rather than force someone to dig into React's exact behavior or worry about whether that behavior could change in the future, I would do an actual change to the state.

How to use React Hooks Context with multiple values for Providers

What is the best way to share some global values and functions in react?
Now i have one ContextProvider with all of them inside:
<AllContext.Provider
value={{
setProfile, // second function that changes profile object using useState to false or updated value
profileReload, // function that triggers fetch profile object from server
deviceTheme, // object
setDeviceTheme, // second function that changes theme object using useState to false or updated value
clickEvent, // click event
usePopup, // second function of useState that trigers some popup
popup, // Just pass this to usePopup component
windowSize, // manyUpdates on resize (like 30 a sec, but maybe can debounce)
windowScroll // manyUpdates on resize (like 30 a sec, but maybe can debounce)
}}
>
But like sad in docs:
Because context uses reference identity to determine when to re-render, there are some gotchas that could trigger unintentional renders in consumers when a provider’s parent re-renders. For example, the code below will re-render all consumers every time the Provider re-renders because a new object is always created for value:
This is bad:
<Provider value={{something: 'something'}}>
This is ok:
this.state = {
value: {something: 'something'},
};
<Provider value={this.state.value}>
I imagine that in future i will have maybe up to 30 context providers and it's not very friendly :/
So how can i pass this global values and functions to components? I can just
Create separate contextProvider for everything.
Group something that used together like profile and it's functions,
theme and it's functions (what about reference identity than?)
Maybe group only functions because thay dont change itself? what
about reference identity than?)
Other simpliest way?
Examples of what i use in Provider:
// Resize
const [windowSize, windowSizeSet] = useState({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight
})
// profileReload
const profileReload = async () => {
let profileData = await fetch('/profile')
profileData = await profileData.json()
if (profileData.error)
return usePopup({ type: 'error', message: profileData.error })
if (localStorage.getItem('deviceTheme')) {
setDeviceTheme(JSON.parse(localStorage.getItem('deviceTheme')))
} else if (profileData.theme) {
setDeviceTheme(JSON.parse(JSON.stringify(profileData.theme)))
} else {
setDeviceTheme(settings.defaultTheme)
}
setProfile(profileData)
}
// Click event for menu close if clicked outside somewhere and other
const [clickEvent, setClickEvent] = useState(false)
const handleClick = event => {
setClickEvent(event)
}
// Or in some component user can change theme just like that
setDeviceTheme({color: red})
The main consideration (from a performance standpoint) for what to group together is less about which ones are used together and more about which ones change together. For things that are mostly set into context once (or at least very infrequently), you can probably keep them all together without any issue. But if there are some things mixed in that change much more frequently, it may be worth separating them out.
For instance, I would expect deviceTheme to be fairly static for a given user and probably used by a large number of components. I would guess that popup might be managing something about whether you currently have a popup window open, so it probably changes with every action related to opening/closing popups. If popup and deviceTheme are bundled in the same context, then every time popup changes it will cause all the components dependent on deviceTheme to also re-render. So I would probably have a separate PopupContext. windowSize and windowScroll would likely have similar issues. What exact approach to use gets deeper into opinion-land, but you could have an AppContext for the infrequently changing pieces and then more specific contexts for things that change more often.
The following CodeSandbox provides a demonstration of the interaction between useState and useContext with context divided a few different ways and some buttons to update the state that is held in context.
You can go to this URL to view the result in a full browser window. I encourage you to first get a handle for how the result works and then look at the code and experiment with it if there are other scenarios you want to understand.
This answer already does a good job at explaining how the context can be structured to be more efficient. But the final goal is to make context consumers be updated only when needed. It depends on specific case whether it's preferable to have single or multiple contexts.
At this point the problem is common for most global state React implementations, e.g. Redux. And a common solution is to make consumer components update only when needed with React.PureComponent, React.memo or shouldComponentUpdate hook:
const SomeComponent = memo(({ theme }) => <div>{theme}</div>);
...
<AllContext>
{({ deviceTheme }) => <SomeComponent theme={deviceTheme}/>
</AllContext>
SomeComponent will be re-rendered only on deviceTheme updates, even if the context or parent component is updated. This may or may not be desirable.
The answer by Ryan is fantastic and you should consider that while designing how to structure the context provider hierarchy.
I've come up with a solution which you can use to update multiple values in provider with having many useStates
Example :
const TestingContext = createContext()
const TestingComponent = () => {
const {data, setData} = useContext(TestingContext)
const {value1} = data
return (
<div>
{value1} is here
<button onClick={() => setData('value1', 'newline value')}>
Change value 1
</button>
</div>
)
}
const App = () => {
const values = {
value1: 'testing1',
value2: 'testing1',
value3: 'testing1',
value4: 'testing1',
value5: 'testing1',
}
const [data, setData] = useState(values)
const changeValues = (property, value) => {
setData({
...data,
[property]: value
})
}
return (
<TestingContext.Provider value={{data, setData: changeValues}}>
<TestingComponent/>
{/* more components here which want to have access to these values and want to change them*/}
</TestingContext.Provider>
)
}
You can still combine them! If you are concerned about performance, you can create the object earlier. I don't know if the values you use change, if they do not it is quite easy:
state = {
allContextValue: {
setProfile,
profileReload,
deviceTheme,
setDeviceTheme,
clickEvent,
usePopup,
popup,
windowSize
}
}
render() {
return <AllContext.Provider value={this.state.allContextValue}>...</AllContext>;
}
Whenever you then want to update any of the values you need to do I like this, though:
this.setState({
allContextValue: {
...this.state.allContextValue,
usePopup: true,
},
});
This will be both performant, and relatively easy as well :)
Splitting those up might speed up a little bit, but I would only do that as soon as you find it is actually slow, and only for parts of your context that would have a lot of consumers.
Still, if your value does not change a lot, there is really nothing to worry about.
Based on Koushik's answer I made my own typescipt version.
import React from "react"
type TestingContextType = {
value1?: string,
value2?: string,
value3?: string,
value4?: string,
value5?: string,
}
const contextDefaultValues = {
data: {
value1: 'testing1',
value2: 'testing1',
value3: 'testing1',
value4: 'testing1',
value5: 'testing1'
} as TestingContextType,
setData: (state: TestingContextType) => {}
};
const TestingContext = React.createContext(contextDefaultValues);
const TestingComponent = () => {
const {data, setData} = React.useContext(TestingContext);
const {value1} = data
return (
<div>
{value1} is here
<button onClick={() => setData({ value1 : 'newline value' })}>
Change value 1
</button>
</div>
)
}
const App = () => {
const [data, setData] = React.useState(contextDefaultValues.data)
const changeValues = (value : TestingContextType) => setData(data && value);
return (
<TestingContext.Provider value={{data, setData: changeValues}}>
<TestingComponent/>
{/* more components here which want to have access to these values and want to change them*/}
</TestingContext.Provider>
)
}

Resources