Using Mobx observables with React dependencies - reactjs

Mobx docs says to use pattern useEffect(reaction(..)) to track observable changes, which looks like some cross-breeding to me. Whats the problem of using react dependencies array to achieve that? I did a basic test, and it works as intended:
const Hello = observer(() => {
const {
participantStore: { audioDisabled },
} = useStores();
useEffect(() => {
console.log('changed', audioDisabled);
}, [audioDisabled]);
return <h1>TEST ME</h1>;
});

There is absolutely no problem with using React things like useEffect, useMemo and etc with MobX. You just need to list all dependencies and if there are many of them maybe it is easier to use reaction or autorun.
So feel free to use whatever way you like more.

Mixing MobX observables and hooks can be mostly painless, but there are a few things to look out for. The most common thing to watch out for is that adding observable values to the dependencies array will not always trigger the effect. Under the surface, MobX objects and arrays are stable references to proxy objects. So even though the value of the observable changes, the useEffect hook doesn't always pick up on that change.
The solution is simple, and is outlined in the MobX Docs under React Integration: useEffect and observables:
Using useEffect requires specifying dependencies. With MobX that isn't really needed, since MobX has already a way to automatically determine the dependencies of an effect, autorun. Combining autorun and coupling it to the life-cycle of the component using useEffect is luckily straightforward
In practice, this looks like:
const Hello = observer(() => {
const { participantStore } = useStores();
useEffect(() => autorun(() => {
console.log('changed', participantStore.audioDisabled);
}), []);
return <h1>Audio is {!participantStore.audioDisabled && 'NOT '}Disabled</h1>;
});
There are a few things to be mindful of:
If you are also dealing with local state (via a useState or other hook), you will need to include those in the dependencies array.
Be sure to return the autorun function from your useEffect hook. This ensures that the listener is de-registered on component unmount.
Your linter will likely get frustrated with missing dependencies. I won't make a recommendation either way since there are pros and cons to either disabling it globally, or keeping it enabled and disabling it per-line. It largely depends on how much local state you will be dealing with.

Related

Alternative way to handle state in React using memo

When it comes to handling complex state in React everybody suggests to flatten the state to avoid something like this just to update a single property:
setState({ …state, user:{ …state.user, profile:{…state.user.profile, address:{…state.user.profile.address, city:’Newyork’}} }});
Which is really cubersome to work with. There is another way: use an object holding your state and return that from a memoized function. Then whenever you made a change simply force a re-render.
// note the reference cannot be changed, but values can.
const data = useMemo(() => ({
user: {
name: "",
profile: {
address: {
city: "New york"
}
}
}
}), []);
// use dummy data to trigger an update
const [toggle, setToggle] = useState(false);
function forceUpdate() {
setToggle(prev => !prev);
}
function makeChanges() {
// make any change on data without any copying.
data.user.address.city = "new city name";
// hydrate the changes to the view when you're done
forceUpdate();
}
return (
<div onClick={() => makeChanges()}>{data.user.address.city }</div>
)
Which works perfectly. Even with massive and complex data structures.
From what I can tell state is really just a memoized values which will trigger an update upon change.
So, my one question: What is the downside of using this?
The docs say useMemo is not a guarantee:
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values [...]
If you'd really want to do something like this and you are absolutely positively sure you're willing to do things unlike anyone else in React land, you'd use useRef for state storage that doesn't cause rerenders by itself. I'm not going to add an example of that, because I don't recommend it in the least.
You should also note that your method will not cause memoized (React.memo()) components to rerender, since they will not "see" changes to props if their identity does not change. Similarly, if another component uses one of your internally mutated objects as a dependency for e.g. an effect, those effects will not fire. Finding bugs caused by that will be spectacularly annoying.
If modifying deep object structures is otherwise cumbersome, see e.g. the Immer library, which does Proxy magic internally to let you modify deep objects without trouble – or maybe immutability-helper if you're feeling more old-school.

React won't let me use `useEffect` in a completely reasonable way

I created the following helper functions because functional components in React do not have mount and unmount events. I don't care what people say; useEffect is not a an equivalent. It can be as I demonstrate below:
//eslint-disable-next-line
export const useMount = callback => useEffect(callback, []);
//eslint-disable-next-line
export const useUnmount = callback => useEffect(() => callback, []);
React does not let me do this because I am technically calling useEffect from a non-component function. I'm doing this because when I use useEffect as a mount or unmount event, it pollutes my terminal with meaningless warnings about not including something in the dependency list. I know, I should be doing this...
export default function MusicPlayback(...) {
...
useEffect(() => stopMusic, []);
...
}
But then I get a warning about stopMusic not being included as a dependency. But I don't want it to be a dependency because then useEffect will no longer be an unmount event and stopMusic will be called on every render.
I know that it is eslint that is warning me and I can use //eslint-disable-next-line but that is too ugly to have in every single file that needs an unmount handler.
To my knowledge there is no way to have an unmount handler without using //eslint-disable-next-line absolutely everywhere. Is there some way around this?
Ok, the dependency check is there for a reason, even when you think it shouldn't be there.
useEffect(() => {
stopMusic()
...
}, [stopMusic, ...])
Let's talk about stopMusic, suppose this is a global function from another third party. If the instance never changes, then you should fire it as a dependency, since it won't hurt.
And if the stopMusic instance does change, then you need to ask yourself why you don't want to put it as a dependency, because it might be accidentally calling an old stopMusic.
Now, suppose you are good with all these and still don't want it to be wired with stopMusic, then consider use a ref.
const ref = useRef({ stopMusic })
useEffect(() => ref.current.stopMusic(), [ref])
Either way you get the point, it has to depend on something, maybe your business logic doesn't want to. But technically as long as you need to invoke something which isn't part of the useEffect, it needs to be a dependency. Otherwise from the useEffect perspective, it's an out-of-sync issue. The point of ref (or any object) is to get into this out-of-sync deliberately.
Of course, if you really hate this linter rule, i believe you can disable it.
NOTE
React community is proposing a way in the future to add these dependencies for you behind your back. The rational behind it is that React is designed to be reactive to the data in one-way train.
This is what I ended up having to do to stop the music on unmount.
export default function MusicPlayback(...) {
const [playMusic, stopMusic] = useMagicalSoundHookThingy(myMusic);
const stopMusicRef = useRef(stopMusic);
stopMusicRef.current = stopMusic; // Gotta do this because stopMusic no longer works after render.
...
useEffect(() => {
const stopMusicInHere = stopMusicRef.current; // Doing this to avoid a warning telling me that when called the ref will probably no longer be around.
return stopMusicInHere;
}, [stopMusicRef]);
...
}
Using a ref like this isn't meaningful. It is just a clever hack to fool eslint. We are packaging something that changes on every render into something that doesn't. That's all we are doing.
The problem I am having is that the entity I'm interacting with is static. But the means to communicate with that entity (namely the function stopMusic) is transient. So the brute force means by which useEffect determines dependence isn't nuanced enough to really indicate whether some dependency has actually changed. Just the tiddlywinks that invoke that dependency, the functions and object created by the hooks. Perhaps this is the fault of the hook writer. Maybe the tiddlywinks should maintain the same life cycle as the entity.
I love React very much, but this is an annoyance I've had for a while, and I'm tired of people telling me that I should just include all the dependencies eslint demands as if I don't really understand what dependencies are actually involved. It is probably ideal to never have any side effects at all in a React program, and rely on a data repository pipeline like Redux to provide any context. But this is the real world and there will always be entities with disconnected lifecycles. Music playing in the background is one such entity. Such is life.

React hooks appends unwanted value with eslint-plugin-react-hooks#next

I have a useEffect hook in my component which call a Redux action to fetch some data.
useEffect(
() => {
props.getClientSummaryAction();
},[]
);
When I go to save the file the linter does this.
useEffect(
() => {
props.getClientSummaryAction();
}, [props]
);
Which obviously send my component into an infinite loop as getClientSummaryAction fetches some data which updates the props.
I have used deconstruction like this and the linter updates the array.
const { getClientSummaryAction } = props;
useEffect(
() => {
getClientSummaryAction();
},
[getClientSummaryAction]
);
Which I am fine with but it doesn't really make sense because getClientSummaryAction will obviously never be updated, its a function.
I just want to know why the linter is doing this and whether this is best practice.
It's not unwanted. You know for a fact that the dependency is not going to change, but React can possibly know that. The rule is pretty clear:
Either pass an array containing all dependencies or don't pass anything to the second argument and let React keep track of changes.
Pass the second argument to useEffect is to tell React that you are in charge of hook's call. So when you don't pass a dependency that is included inside your effect eslint will see this as a possible memory leak and will warn you. DO NOT disable the rule just continue to pass the dependency as you are already doing. May feel redundant but it's for the best.

is it safe to ignore react's warning about calling the useState hook conditionally when only the parameter is conditional?

I am creating a calendar date selection function component for assigning days to schedules in my React app and I wanted to be able to pre-populate calendar with the existing data so that it could be modified by the user.
This is what I have so far:
const initialOptions: { [key: string]: number[] } = {};
for (const option of Object.keys(props.options)) {
const dates = props.options[option].dates;
initialOptions[option] = dates ? dates : [];
}
const [selectedDates, setSelectedDates] = useState(initialOptions);
However, when I try and render the page, I get this:
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooks
After reading through the react rules of hooks, I didn't see anything that indicated that react was depending on the value of the parameter to "associate local state with [my useState() call]". All it really said was...
As long as the order of the Hook calls is the same between renders, React can associate some local state with each of them.
So why is react complaining at me when I am calling useState() in top-level react code that is outside of any conditional statements or functions as per their own rules?
The comments on this question that basically said calls to react hooks need to be before any control structures, even if they are unrelated were what pointed me in the right direction.
The answer provided in the comments was not quite satisfactory though since I needed to process the inital value of selectedDates if an initial value was provided and have that available before I called useState() in order to pass it as a parameter.
Despite being perplexed by this and the somewhat nonsensical nature of this solution (order shouldn't matter with two barely-related pieces of code, right?), I managed to refactor my logic such that it both stopped react from complaining AND allowed me to still conditionally set the selectedDates in my react calendar component.
Here's what I ended up with:
const initialOptions: { [key: string]: number[] } = {};
Object.entries(props.options).forEach(value => {
const [id, options] = value;
if (options.dates) {
initialOptions[id] = options.dates;
}
});
const [selectedDates, setSelectedDates] = useState(initialOptions);
As someone who isn't that familiar with the internals of react, it seems that either:
the react team got something wrong when writing the ESLint plugin for the react hook rules, or
there was a functional limitation in how ESLint works that doesn't allow for a more specific/accurate check, causing the developers to go with a "better safe than sorry" approach by using a less specific check that still caught rule violations as well as edge cases like this one
So overall, my conclusion is, by replacing my for loop with a call to .forEach(), the ESLint
plugin saw my loop as a function rather than a control structure/conditional and allowed my code to pass the test and run without issue.
Now, as a self-described "junior" react developer, i'm pretty sure that tricking ESLint into not giving an error like this is not a good long-term solution. Ideally ESLint rules probably need updating to better check for improper use of conditionals, and/or the react docs should be updated with an example for how to conditionally set a hook's default value in a way that doesn't violate the rules of react hooks.
EDIT: I have created an issue for the react documentation in order to find out what a good long-term solution to this would be and get the documentation and/or ESLint plugins updated if necessary
If you ignore the warning that means that you are setting your expectations wrong on how your Component's code will be executed during renderings.
Just by looking at initialOptions, you can see that the initial value is based on incoming props. In React when the props change your Component gets re-rendered, the initialOptions is re-evaluated BUT it's NOT updated again by useState(initialOptions).
Sure you can say: "but my useState(initialOptions) is not wrapped around any condition!". While that is absolutely true, you didn't inform React that selectedDates needs to be updated between renders. It's value is still the first initial value when the Component was rendered first time.
You need to move the foreach logic into a useEffect with dependency to props.options.
Example based on your code:
const initialOptions: { [key: string]: number[] } = {};
const [selectedDates, setSelectedDates] = useState(initialOptions);
useEffect(() => {
// Here it's okay to have an optional condition!
if (!props.options.length) { return false; }
const newOptions = [];
Object.entries(props.options).forEach(value => {
const [id, options] = value;
if (options.dates) {
newOptions[id] = options.dates;
}
});
setSelectedDates(newOptions);
}, [props.options]);
I've prepared a sandbox example which demonstrates why the rule "Only Call Hooks at the Top Level - Don’t call Hooks inside loops, conditions, or nested functions." must be respected https://codesandbox.io/s/immutable-meadow-xvy50t?file=/src/Component.js <-- click the body of the page repeatedly and notice that the component doesn't update.

What is the proper way to duplicate componentDidMount with hooks?

I'm having trouble understanding the proper way to recreate the behavior of the componentDidMount life cycle function using react hooks.
I have found the generally accepted method is like so:
useEffect(() => {
//do componentDidMount stuff here
}, []);
However, when theres additional parameters, other dependencies, etc. I get linting errors, as in this example:
useEffect(() => {
fetchData(design, onSuccess, onError);
}, []);
That one throws linting errors. What would be the proper way to handle that type of scenario? I'd like to avoid disabling eslint.
React Hook useEffect has missing dependencies: 'design' and 'onSuccess'. Either include them or remove the dependency array react-hooks/exhaustive-deps
You can take a look at this issue. I found it very interesting.
You can also take a look at Two Ways to Be Honest About Dependencies that Aron mentions on his answer. It's very interesting and goot to understand hooks dependencies.
I'd like to avoid disabling eslint.
So to do that, here is what you need to do.
In the issue, some one gives an example where he calls a function from outside of the useEffect.
const hideSelf = () => {
// In our case, this simply dispatches a Redux action
};
// Automatically hide the notification
useEffect(() => {
setTimeout(() => {
hideSelf();
}, 15000);
}, []);
And by reading all the comments and looking at Dan Abramov comment
... But in this specific example the idiomatic solution is to put hideSelf inside the effect
So this means doing
// Automatically hide the notification
useEffect(() => {
const hideSelf = () => {
// In our case, this simply dispatches a Redux action
};
setTimeout(() => {
hideSelf();
}, 15000);
}, []);
This an example where you can solve the problem without using disableling eslint.
If this isn't your case (maybe you use Redux or something alike) you should put it as a deppendency of the effect
... If it dispatches a Redux action then put this action as a dependency
To solve this problem, it deppends alot of your situation. You didn't give us a clear example of what is your case, so I found one an give you a generic solution.
Short answer
Add everything that is outside of the effect in the effect dependency (inside [])
OR
Declare the functions that are outside of the effect inside of it.
I'm guessing you're getting the exhaustive-deps error?
When using a useEffect the recommendation is to put all values that are used in the effect in the dependencies array, so that you are being "honest" about which values the effect uses. Dan Abramov talks about this here https://overreacted.io/a-complete-guide-to-useeffect/#two-ways-to-be-honest-about-dependencies.
However if you are happy to ignore this and are sure that you only want this effect to run the first time this component renders then you can safely ignore the lint errors using // eslint-disable-line exhaustive-deps.
EDIT: There isn't really a way round this because ultimately you are not being "honest" about your deps, strictly speaking.

Resources