Most examples for using RxJS to observe button clicks are like:
var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
.subscribe(() => console.log('Clicked!'));
Is this okay in React? Since there is the virtual DOM, is it still okay to get reference to the real DOM like this? What happens after a re-render? Is the subscription lost?
I.e. they use document.querySelector.
But when I write my render() method, I'm used to <button onClick={...} >.
What is the recommended way to do this in React? Get rid of the onClick inside the JSX, add a class/id to my button, or maybe a ref, and use the style above?
Also, should I unsubscribe anywhere, e.g. in componentWillUnmount()? In which case I'd want to keep references to all my subscribers (event listeners)? In which case this seem much more complex than the old (callback) way.
Any suggestions, thoughts on this?
I've thought about this a lot - I think the answer by #ArtemDevAkk is one really good way to do it, but I'm not sure it accounts for how you intend to use the data. I'd like to suggest an alternative approach. Admittedly, I've used hooks for this, but you could do this in old school classes in a similar way.
I've had to break up your question into a few parts:
How to reference a DOM node in React
How to create a Subject and make it fire
How to subscribe to any Observable in React (useEffect)
Getting a reference to a DOM node
In case all you are asking is how to reference a DOM node, the rule of thumb is that to access the DOM in React, you use the useRef hook (or createRef in a Class).
Keeping your Subject active (for sharing events)
The benefit of this method is that your Subject will be created once and kept alive indefinitely, so anything is able to subscribe and unsubscribe at will. I can't think of a great reason for using RxJs in a React project because React has its own ways to handle events, so it's hard to know if this will solve your problem.
function MyRxJsComponent() {
// establish a stateful subject, so it lives as long as your component does
const [ clicks$ ] = useState(new Subject());
// register an event handler on your button click that calls next on your Subject
return <button onClick={e => clicks$.next(e)}></button>
}
This will create one Subject that will stay alive as long as your component is alive on the page. My best guess for how you might use this is by using a Context to contain your subject (put clicks$ in your context) and then pushing events to that Subject.
Note how I'm not using fromEvent to create an Observable, because this Observable would be created and destroyed with your button, making it impossible for anything to stay subscribed to it outside the component.
Just using an Observable internally
Alternatively (as #ArtemDevAkk alluded to), you could just create your Observable for local use and accept the limitation that it will be created and destroyed (this might actually be preferable, but again, it depends on what you're actually trying to achieve).
function MyRxJsComponent() {
// get a reference to the button (use <button ref={buttonRef} in your component)
const buttonRef = useRef(null)
// use useEffect to tell React that something outside of its
// control is going to happen (note how the return is _another_
// function that unsubscribes.
useEffect( () => {
// create the observable from the buttonRef we created
const clicks$ = fromEvent(buttonRef.current, 'click')
clicks$.subscribe( click => {
// do whatever you need to with the click event
console.log('button was clicked!', click)
})
return () => {
start$.unsubscribe()
}
}, [buttonRef]);
return <button ref={buttonRef}>Click me!</button>;
}
But should you?
I think overall, I've shown above how you can use state to keep a Subject alive for as long as you need; you can use useEffect() to subscribe to an observable and unsubscribe when you don't need it any more. (I haven't tested the samples locally, so there might be a small tweak needed..)
I mentioned using a Context to share your Subject with other components, which is one way I can imagine RxJs being useful in React.
Ultimately, though, if this is all you want to do, there are better ways to handle events in React. A simple <button onClick={() => console.log('clicked')}> could be all you need. React, being a reactive library in itself, isn't really designed to contain the amount of state that an Observable can contain - your views are meant to be simplified projections of state stored elsewhere. For the same token, it's generally advised that you don't try to reference a specific element unless you have an exceptional reason to do so.
const buttonRef = useRef(null)
useEffect( () => {
const start$ = fromEvent(buttonRef.current, 'click').subscribe( click => {
console.log('click event :', click)
})
return () => {
start$.unsubscribe()
}
}, [])
add ref to button
Related
The standard way to make an API call in functional React is with useEffect:
function Pizzeria() {
const [pizzas, setPizzas] = useState([])
useEffect(
() => fetchPizzas().then(setPizzas),
[]
)
return (
<div>
{pizzas.map((p, i) => <Pizza pizza={p} key={i} />)}
</div>
)
}
But, as this article points out, useEffect will not fire until after the component has rendered (the first time). Obviously in this trivial case it makes no difference, but in general, it would be better to kick off my async network call as soon as possible.
In a class component, I could theoretically use componentWillMount for this. In functional React, it seems like a useRef-based solution could work. (Allegedly, tanstack's useQuery hook, and probably other libraries, also do this.)
But componentWillMount is deprecated. Is there a reason why I should not do this? If not, what is the best way in functional React to achieve the effect of starting an async call early as possible (which subsequently sets state on the mounted component)? What are the pitfalls?
You're splitting milliseconds here, componentWillMount/render/useEffect all happen at essentially the same time, and the time spent fetching occurs after that. The difference in time from before to after rendering is tiny compared to the time waiting for the network when the request is sent. If you can do the fetch before the component renders, react-query's usePrefetch is nice for that.
Considering the scope of a single component, the earliest possible would be to just make the call in the component's function. The issue here is just that such statement would be executed during every render.
To avoid those new executions, you must keep some kind of "state" (or variable, if you will). You'll need that to mark that the call has been made and shouldn't be made again.
To keep such "state" you can use a useState or, yes, a useRef:
function Pizzeria() {
const pizzasFetchedRef = useRef(false)
const [pizzas, setPizzas] = useState([])
if (!pizzasFetchedRef.current) {
fetchPizzas().then(setPizzas);
pizzasFetchedRef.current = true;
}
Refs are preferred over state for this since you are not rendering the value of pizzasFetched.
The long story...
Yet, even if you use a ref (or state) as above, you'll probably want to use an effect anyway, just to avoid leaks during the unmounting of the component. Something like this:
function Pizzeria() {
const pizzasFetchStatusRef = useRef('pending'); // pending | requested | unmounted
const [pizzas, setPizzas] = useState([])
if (pizzasFetchStatusRef.current === 'pending') {
pizzasFetchStatusRef.current = 'requested';
fetchPizzas().then((data) => {
if (pizzasFetchStatusRef.current !== 'unmounted') {
setPizzas(data);
}
});
}
useEffect(() => {
return () => {
pizzasFetchStatusRef.current = 'unmounted';
};
}, []);
That's a lot of obvious boilerplate. If you do use such pattern, then creating a custom hook with it is the better way. But, yeah, this is natural in the current state of React hooks. See the new docs on fetching data for more info.
One final note: we don't see this issue you pose around much because that's nearly a micro-optimization. In reality, in scenarios where this kind of squeezing is needed, other techniques are used, such as SSR. And in SSR the initial list of pizzas will be sent as prop to the component anyway (and then an effect -- or other query library -- will be used to hydrate post-mount), so there will be no such hurry for that first call.
I have code example like this (codesandbox demo):
const [array, setArray] = useState([]);
const addElementToArray = () => {
array.push(1);
setArray([...array]);
}
return (
<div className="App">
<button onClick={addElementToArray}>addElementToArray</button>
{array.map(el => <div>{el}</div>)}
</div >
);
Here I dont use standard way of updating array like
setArray([...array, 1]);
What disadvantages of updating values like in my first example?
In this example you won't notice any problems. But then, this is a very small and contrived example, isn't it?
But in a larger application with more complex logic and functionality, this can easily lead to strange and difficult bugs. Why? Because you're directly mutating state:
array.push(1);
Does anything else hold a reference to that state or observe it in any way? Is this mutation happening in the middle of a component render, where some logic has already responded to the pre-mutated state and the remaining logic will respond to the post-mutated state? Are multiple state updates being queued and now we don't know which ones will see which version of state?
That's a lot to have to keep track of. Or you can avoid all of those potential problems by simply not mutating state and sticking with "the standard way" that you already know:
setArray([...array, 1]);
or, if there may be multiple state updates and you want to append the 1 to the most current:
setArray(a => [...a, 1]);
If you use this line alone,
array.push(1);
the state changes but React isn't aware of the change so the component won't rerender. In this case, the whole purpose of React is gone because the UI does not correspond to the state change, and that makes it a bad practice.
However, if you use this line alone:
setArray([...array, 1]);
the UI will correspond to the state change, which is the correct way of using React.
And regarding this:
array.push(1);
setArray([...array]);
you're basically mocking React because .push() changes the state without any rerender.
I have just found out that I can use data hooks in createSelector functions and it works. An example:
// This is a normal hook
const useUserReducer = () => {
const userAccessData = useSelector(state => state?.userAccessData)
return userAccessData
}
// Here I use the hook as first argument!
export const useUserReducerFromCreateSelector = createSelector(useUserReducer, (result) => {
console.log(result) // userAccessData printed correctly
return result
})
Then I use it in my component as a normal hook:
const Component = () => {
const result = useUserReducerFromCreateSelector([])
console.log(result) // userAccessData printed correctly
return (
<>
{JSON.stringify(result)}
</>
)
}
I dont see any documentation about this, so I wonder if its safe to use it. It would help me a lot creating reusable selectors.
(I tested while changing the state at various points in time and I always see the correct state)
It is certainly an abuse if it is working. createSelector is only supposed to be a pure state selector function, so naming the returned selector function like a React hook, i.e. useUserReducerFromCreateSelector, is likely to cause some linter warnings eventually.
The potential issue is that Reselect and createSelector creates memoized selector functions. If the input value to a selector doesn't change, then the selector function returns the previously computed selector value. This means that a selector using a React hook like this potentially conditionally calling a React hook which is a violation of the Rules of Hooks.
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function,
before any early returns. By following this rule, you ensure that
Hooks are called in the same order each time a component renders.
That’s what allows React to correctly preserve the state of Hooks
between multiple useState and useEffect calls. (If you’re curious,
we’ll explain this in depth below.)
Only Call Hooks from React Functions
Don’t call Hooks from regular JavaScript functions. Instead, you
can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks (we’ll learn about them on the next page).
By following this rule, you ensure that all stateful logic in a
component is clearly visible from its source code.
I don't consider it safe to use any React hook in a selector function like this.
Split out the logic of selecting the state from the useUserReducerFromCreateSelector hook to be used in your selector functions.
Example:
const userAccessData = state => state?.userAccessData || {};
const computedUserAccessData = createSelector(
[userAccessData],
data => {
// logic to compute derived state, etc...
return newUserAccessData;
},
);
I was really intrigued seeing this particular use in the redux-toolkit github repo issues and nobody complaining about it so I decided to ask the same question in the reselect github page.
Here is the response of Mark Erikson (redux maintainer):
No, this is not safe!
You're technically getting away with it because of how you're using
that in a component. But if you were to try to use that selector
outside of a component, it would break.
I'd really recommend sticking with keeping these concepts separate.
Write and name selectors as selectors. Write and name hooks as hooks.
Don't try and mix the two :)
To be clear, the code that you wrote above should run. It's ultimately
"just" composition of functions and calling them in a particular
order.
But given how hooks work, and how selectors work, it's best to keep
those concepts separate when writing the code to avoid confusion.
I just started using hooks in react and am creating a prototype custom hook for a framework.
The hook should take an object as an argument for initialization and cleanup (setting up/removing callbacks for example).
Here is my simplified Code so far:
export function useManager(InitObj) {
const [manager] = useState(() => new Manager());
useEffect(() => {
manager.addRefs(InitObj)
return () => manager.removeRefs(InitObj)
}, [manager]);
return manager;
}
to be used like this:
useManager({ cb1: setData1, cb2: setData2... })
In future Iterations the Manager might be a shared instance, so I need to be able to be specific about what I remove upon cleanup.
I put console.log all over the place to see If i correctly understand which code will be run during a render call. From what I can tell this code does 100% what I expeted it to do!
Unfortunately (and understandably) I get a warning because I did not include InitObj in the effects dependencies. But since I get an object literal simply putting it in there will cause the effect to be cleaned up/rerun on every render call since {} != {} which would be completely unnecessary.
My research so far only revealed blog posts like this one, but here only primitive data is used that is easily classified as "the same" (1 == 1)
So far I have found 3 possible solutions that I am not completely happy with:
using useMemo to memoize the object literal outside the hook
useManager(useMemo(() => { cb: setData }, []))
This adds more responsibility on the developer using my code => not desirable!
using useState inside the hook
const [iniOBj] = useState(InitObj);
A lot better already, but it adds state that does not feel like state. And it costs (minimal) execution time and memory, I would like to avoid that if possible.
using // eslint-disable-next-line react-hooks/exhaustive-deps
Works for sure, but there might still be other dependencies that might be missed if I simply deactivate the warning.
So my question is:
How can I use an object as initializer for custom hooks without adding complexity/state or inviting future problems?
I half expect that the useState option will be my best choice, but since I am new to hooks there might still be something that eluded my understanding so far.
I have a reset button in my app that resets a few variables of my functional component:
const [selectedItem, setSelectedItem] = useState(0);
const [a, setA] = useState('a');
const [b, setB] = useState('blue');
<button onClick={e => ???}>clicky</button>
<button onClick={e => ???}>clicky</button>
There are two ways I could 'reset' the data: monitoring selectedItem for changes using useEffect, or have a handler that does so:
<button onClick ={e => setSelectedItem(e.target.value)} />
useEffect(() => {
setA(Math.random())
setB(Math.random())
}, [selectedItem])
or
<button onClick ={e => handler(e.target.value)} />
const handler = item => {
setSelectedItem(Math.random())
setA(Math.random())
setB(Math.random())
}
What are the practical differences between these approaches? The hooks docs say to use useEffect for performing side effects, but I can't see why this approach wouldn't work as well.
What is the difference between these approaches?
I will try to answer this question in three points.
Mental model
You need to "think in effects". the UseEffect hook lets you perform side-effects that manly need to happen async like (fetch Data from API, manipulate the DOM).
based on that it's better to use UseEffect to handle side-effects so you are not confusing your colleges.
Async
You need to keep in your mind that useEffect is an async function but your event handler is sync function. That can lead to totally different behavior maybe you are not seeing a weird behavior here but maybe in other examples, you will start to notice that.
React mechanism
the last difference to notice it you need to understand React update state mechanism, react makes patches to update the state. That means in your event handler the three-state will cause one re-render because they will happen at the same time. In your useEffect that is not the case, you are updating one of them that case re-render then you are performing the effect that will case new re-render.
Maybe there are other differences but that what can I see right now.
I hope it’s a useful answer.
There are a few peculiar differences between the above two methods.
In the first method of using useEffect, you would be updating states a and b whenever selectedItem changes, be it by a button click or some other sideeffect such as a prop change. However in the second case, states a and b would only be updated if selectedItem is updated on button click and you would need to call setA and setB to update states everywhere you update selectedItem separately
Secondly, when you are using a useEffect to update state, the state update will happen after updating selectedItem, however in the second case state updates doesn't gurantee that selectedItem is updated before setting the other states and hence if the other state updated depend on selectedItem value, you need to pass the updated selectedItem value to the other state updaters separately
In short, making use of useEffect is better when you know you have to take other actions whenever a state change occurs no matter how it occurs. Also its useful when you want to take action after a particular state is updated.
I believe it's important to consider the semantics of what you're doing. For example:
<button onClick ={e => handler(e.target.value)} />
const handler = item => {
setSelectedItem(Math.random())
setA(Math.random())
setB(Math.random())
}
this means that whenever you click the button you want the 3 state variables to be changed.
On the other hand:
<button onClick ={e => setSelectedItem(e.target.value)} />
useEffect(() => {
setA(Math.random())
setB(Math.random())
}, [selectedItem])
this means whenever you click the button you want that one state variable to change and independently of that you want, whenever that one state variable changes, to change those other two state variables.
The real question you should be asking is what is it you really want to express with your code, given that it has the same end result. In short, what makes semantic sense to you? Does it make sense to say "this button can be used to change those 3 state variables" or does it make more sense to say "this button can be used to chanage the selectedItem state variable and this entire component will change the a and b state variables whenever the selectedItem changes?
It is usually important to make sure your code makes semantic sense so you don't land in the pitfalls of getting unintended side-effects when you make code changes. For example, if selectedItem ends up being changeable by other means, the 2nd method will ensure that a and b change at the same time. Do you really want that?
There's also a practical consideration. There's the eslint rule called react/no-did-update-set-state which states:
Updating the state after a component update will trigger a second render() call and can lead to property/layout thrashing.
Layout thrashing basically means there's multiple potential redraws of the layout before a user can interact with it again. In the case of useEffect this can be an issue because useEffect is triggered after a layout update and setting the state might trigger another one. It usually has no noticeable effect on very simple operations but if you have a complex component hierarchy and end up re-rendering large portions of it then you will end up with a less responsive layout.
There's also the additional consideration that with the useEffect you also need to be mindful to avoid cyclical dependency changes e.g. selectedItem changes a and a changes selectedItem or makes a change which ends up changing selectedItem somewhere further down the line.
So overall there are three notes:
Use whichever makes more semantic sense for your component
Be aware of potential layout thrashing
If useEffect does make more sense take a step back and really think about why it makes more sense and whether there is a better way to solve your problem and at the same time avoid using useEffect to set state variables.
Ok the scenario you are referring to here is not really a side-effect, or I would say the side-effect react refers to. React refers to side-effect like if you are doing a network request
In your case if you just want to reset some variables I think having a clickHandler is the way to do, you would use a useEffect like if you want to do a network request when the component loads or some props change
Hope it clarifies