Would this be considered "breaking the rules of hooks"? - javascript

I have a compound component that is used to build a bunch of different (but similar) screens in a react native app. Data comes from a CMS, and depending on the value of a prop called type it needs to have different state variables, different validation, etc. I've written a config object who's methods map to the values of type, and contain the state and functions needed for each use case. The pattern looks like this (edited for example sake):
import { useState } from 'react';
const MyComponent = props => {
const { type } = props; // possible values of type are 'A', 'B', 'C'
const config = {
A() {
const [value, setValue] = useState('');
function onChange({ target }) {
setValue(target.value);
}
return {
value,
onChange
}
},
B() {
// ...
return {
value: '',
onChange: () => {}
}
},
C() {
// ...
return {
value: '',
onChange: () => {}
}
}
}[type]();
return <input value={config.value} onChange={config.onChange} />
};
export default MyComponent;
BUT!
In the react docs it says this:
Don’t call Hooks inside loops, conditions, or nested functions.
My question is - does the above example violate the rules of hooks? It seems to work for what I need it to, but I suspect that it may cause problems. Would appreciate any thoughts / discussion. Cheers!

Short answer: yes.
Long answer:
React depends on hooks to be called in the exact same order between renders (I believe internally the hooks are stored in a linked list, which is altered on mounting and unmounting of the component, if the order changes between renders without a mount and unmount, React has no way to know which hook belongs to which component).
Because your rendering from a CMS, the value of type is unlikely to change between renders, so you probably will never get in to trouble with this particular use case, but it is a bad habit to get in to, and there is almost certainly a better way to handle what you are trying to do. Maybe write a hook that takes type as an argument?
function useMyHook(type:string) {
const [state, setState] = useState('');
const onChange = useMemo(() => {
if (type = 'A')
return () => doXyz()
}, [type])
return {state, onChange}
}
This is contrived, but hopefully will get you thinking of alternate approaches to your problem.

Related

React fast global redux-like variable

I'm making a front-end application using react/webgl and I need to be vary of performance improvements since almost everything must be rendered real-time and dynamically.
I need to render something on a canvas and need to use some variable's globally across many different components, but they need to be updated fast. Technically redux is what I need, however accessing dispatched variables takes time and causes crucial performance issues.
So instead I opted in to use useRef() which solves the “slow” issue but now I cannot update it’s value across different components. Using useRef() solves my issue but since it's not globally accessible it causes problems on other parts of the application.
Declaration of the variable looks like this:
import { useRef } from 'react';
const WebGLStarter = (props) => {
...
const myValue = useRef();
myValue.current = someCalculation();
function render(myValue.current){
...
requestAnimationFrame(function () {
render(myValue.current);
});
}
...
}
Currently someCalculation() is on the same component as it's declaration. I want to use someCalculation() on a different file but I can't do it beacuse useRef() won't allow me to. And again, I can't use redux because it's slow.
TL;DR : I need something similar to redux but it needs to be fast enough to not cause performance issues on an infinite loop.
Create a context with the ref. Wrap your root with the provider, and use the hook to get access to the ref when you need to use/update it:
import { createContext, useRef, useContext } from 'react';
const defaultValue = /** default value **/;
const MyValueContext = createContext(defaultValue);
const MyValueContextProvider = ({ children }) => {
const myValueRef = useRef(defaultValue);
return (
<MyValueContext.Provider value={myValueRef}>
{children}
</MyValueContext.Provider>
);
};
const useMyValue = () => useContext(MyValueContext);
To use in components call the useMyValue hook. This would give you direct access to the ref, and since you don't update any state (just change the ref's current property) it won't cause re-renders:
const WebGLStarter = (props) => {
const myValue = useMyValue();
myValue.current = someCalculation();
...
};

How to avoiding repeating work (or to keep common/shared state) in nested hooks?

In a nested hook: how could one know if it already was invoked in the current component (instance) and access any previously computed/saved values?
Preferably without the Component author/the hook user having to know about this and not having to do anything special for it to work.
Example
An example to illustrate the problem:
const useNestedHook = () => {
// Some heavy work with the same result for each component instance.
// Is it possible to remember the result of this work when
// this hook is used again in the same component instance?
// So I would like to save a state which all uses of useNestedHook
// could access as long as they are in the same component instance.
}
const useHookA = () => {
useNestedHook();
};
const useHookB = () => {
useNestedHook();
};
const Component = () => {
useHookA();
// Would like to avoid useNestedHook repeating its work since it's
// used in the same component and would have this same result (per definition)
// Preferably without the Component author having to know anything about this.
useHookB();
};
Imagined solution
Something like a "named" shared state, which would give access to the same shared state (in the same component instance) no matter which hook it's used in. With each component instance having its own separate state as usual. Maybe something like:
const [state, setState] = useSharedState("stateId", initialValue);
No, that is not possible. Each useState() call will always be separate from other useState() calls.
The component can not use the hooks as in your example, but the component author doesn't necessarily have to care for the implementation details.
A solution would depend on the use case.
Some details:
One state is defined by where the useState() call is written in the code (see explanation), which is not directly related to the instance. I.e. two useState() calls and two instances are 4 state values.
You can use shared state e.g. using context, but then the state would also be shared by all instances, not only the hooks (which you don't want).
So the useNestedHook() will always be "separate", but if you can use shared state, and you only care for "caching", and can accept that the useNestedHook() is called twice (i.e. skip the expensive operation if the result is the same), then you can use useEffect(). I.e. the call would depend on the value, not the instance and not the hook.
Some examples:
1. One hook with options
E.g. if your hooks A and B would optionally calculate two different values, which need the same useNestedHook() value, you could create one hook with options instead, e.g.:
const useHookAB = ({ A, B }) => {
const expensiveValue = useNestedHook();
if( A ){ /* do what useHookA() was doing */ }
if( B ){ /* do what useHookB() was doing */ }
};
const Component = () => {
useHookAB({ A: true, B: true });
};
I can't imagine another reason right now why you would want to call the hooks like that.
2. The "normal" way
The obvious solution would be:
const useHookA = ( value ) => {
// ...
};
const useHookB = ( value ) => {
// ...
};
const Component = () => {
const value = useNestedHook();
useHookA( value );
useHookB( value );
};
But I can imagine reasons why you can't (or don't like to) do it that way.

How to give react components dynamic ids, when React runs code twice?

It's a known React behavior that code runs twice.
However, I'm creating a form builder in which I need to be able to give each form input a dynamic Id and use that Id for a lot of other purposes later. Here's a simple code of an input:
const Text = ({placeholder}) => {
const [id, setId] = useState(Math.random());
eventEmitter.on('global-event', () => {
var field = document.querySelector(`#${id}`); // here, id is changed
});
}
But since Math.random() is a side-effect, it's called twice and I can't create dynamic ids for my form fields.
The reason I'm using document.querySelector can be read here.
My question is, how can I create consistent dynamic ids for my inputs?
It seems you think that useState(Math.random()); is the side-effect causing you issue, but only functions passed to useState are double-invoked.
I think the issue you have is that the eventEmitter.on call is the unintentional side-effect since the function component body is also double invoked.
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies <-- this
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer <-- not this
To remedy this I believe you should place the eventEmitter.on logic into an useEffect hook with a dependency on the id state. You should also probably use id values that are guaranteed a lot more uniqueness. Don't forget to return a cleanup function from the effect to remove any active event "listeners", either when id updates, or when the component unmounts. This is to help clear out any resource leaks (memory, sockets, etc...).
Example:
import { v4 as uuidV4 } from 'uuid';
const Text = ({placeholder}) => {
const [id, setId] = useState(uuidV4());
useEffect(() => {
const handler = () => {
let field = document.querySelector(`#${id}`);
};
eventEmitter.on('global-event', handler);
return () => {
eventEmitter.removeListener('global-event', handler);
};
}, [id]);
...
}

Asynchronous way to get the latest change in State with React Hooks

I have started learning React and developing an application using ReactJS. Recently i have moved to React Hooks. I know that in Class Component we can get the latest data from the state with the 2nd argument which is present in setState() like
state = {name: ''};
this.setState({name: name}, () => {console.log(this.state)});
I wanted to know if there is any arguments in React Hooks with which we can get the latest data from it.
I am using React Hooks in the below mentioned way, but upon console logging it always return the previous state
Hooks Eg:
const [socialData, setSocialData] = useState([
{ id: new Date().getTime().toString(), ...newItem }
]);
const onChangeCallback = (index, type, data) => {
let newSocialData = [...socialData];
newSocialData[index] = { ...newSocialData[index], [type]: data };
setSocialData(newSocialData);
onInputChange(newSocialData, formKey);
console.log(newSocialData);
};
The this.setState() second argument is not exactly to get the latest data from the state, but to run some code after the state has been effectively changed.
Remember that setting the state is an asynchronous operation. It is because React needs to wait for other potential state change requests so it can optimize changes and perform them in a single DOM update.
With this.setState() you can pass a function as the first argument, which will receive the latest known state value and should return the new state value.
this.setState((previousState) => {
const newState = previousState + 1;
return newState;
}, () => {
console.log('State has been updated!')
});
With that being said, there are special and rare cases when you need to know exactly when the state change has taken place. In many years of working with React I only faced this scenario once and I consider it a desperate attempt to make things work.
Usually, during a callback execution, like your onChangeCallback you want to change the state as the last thing your function does. If you already know the new state value, why do you want to wait for the real state to change to use it?
The new state value should be a problem to be handled during the next render.
If you want to run some code only when that particular state value changes you can do something like this:
import React, {useState, useEffect, useCallback} from 'react';
function MyComponent() {
const [value, setValue] = useState(false);
const onChangeHandler = useCallback((e) => {
setValue(!!e.target.checked);
}, []);
useEffect(() => {
// THIS WILL RUN ONLY WHEN value CHANGES. VERY SIMILAR TO WHAT YOU ARE TRYING TO DO WITH THE this.setState SECOND ARGUMENT.
}, [value]);
return (
<input type='checkbox' onChange={onChangeHandler} />
);
}
There is also a way to create a custom useState hook, to allow you passing a second argument to setValue and mimic the behavior of this.setState, but internally it would do exactly what I did in the above component. Please let me know if you have any doubt.

Storing an object in state of a React component?

Is it possible to store an object in the state of a React component? If yes, then how can we change the value of a key in that object using setState? I think it's not syntactically allowed to write something like:
this.setState({ abc.xyz: 'new value' });
On similar lines, I've another question: Is it okay to have a set of variables in a React component such that they can be used in any method of the component, instead of storing them in a state?
You may create a simple object that holds all these variables and place it at the component level, just like how you would declare any methods on the component.
Its very likely to come across situations where you include a lot of business logic into your code and that requires using many variables whose values are changed by several methods, and you then change the state of the component based on these values.
So, instead of keeping all those variables in the state, you only keep those variables whose values should be directly reflected in the UI.
If this approach is better than the first question I wrote here, then I don't need to store an object in the state.
this.setState({ abc.xyz: 'new value' }); syntax is not allowed.
You have to pass the whole object.
this.setState({abc: {xyz: 'new value'}});
If you have other variables in abc
var abc = this.state.abc;
abc.xyz = 'new value';
this.setState({abc: abc});
You can have ordinary variables, if they don't rely on this.props and this.state.
You can use ES6 spread on previous values in the object to avoid overwrite
this.setState({
abc: {
...this.state.abc,
xyz: 'new value'
}
});
In addition to kiran's post, there's the update helper (formerly a react addon). This can be installed with npm using npm install immutability-helper
import update from 'immutability-helper';
var abc = update(this.state.abc, {
xyz: {$set: 'foo'}
});
this.setState({abc: abc});
This creates a new object with the updated value, and other properties stay the same. This is more useful when you need to do things like push onto an array, and set some other value at the same time. Some people use it everywhere because it provides immutability.
If you do this, you can have the following to make up for the performance of
shouldComponentUpdate: function(nextProps, nextState){
return this.state.abc !== nextState.abc;
// and compare any props that might cause an update
}
UPDATE
This answer is many years old and is now obsolete.
You should probably use hooks and/or refactor your code if you're trying to do a deep update.
You should probably be using Functional Components with useState hooks or a reducer.
I'm keeping this answer alive for historical context.
Beyond here be DRAGONS!
this.setState({abc: {xyz: 'new value'}}); will NOT work, as state.abc will be entirely overwritten, not merged.-
This works for me:
this.setState((previousState) => {
previousState.abc.xyz = 'blurg';
return previousState;
});
Unless I'm reading the docs wrong, Facebook recommends the above format.
https://facebook.github.io/react/docs/component-api.html
Additionally, I guess the most direct way without mutating state is to directly copy by using the ES6 spread/rest operator:
const newState = { ...this.state.abc }; // deconstruct state.abc into a new object-- effectively making a copy
newState.xyz = 'blurg';
this.setState(newState);
Easier way to do it in one line of code
this.setState({ object: { ...this.state.object, objectVarToChange: newData } })
Even though it can be done via immutability-helper or similar I do not wan't to add external dependencies to my code unless I really have to. When I need to do it I use Object.assign. Code:
this.setState({ abc : Object.assign({}, this.state.abc , {xyz: 'new value'})})
Can be used on HTML Event Attributes as well, example:
onChange={e => this.setState({ abc : Object.assign({}, this.state.abc, {xyz : 'new value'})})}
If you want to store an object in the state using functional components you can try the following.
import React from 'react';
import {useState, useEffect} from 'react';
const ObjectState= () => {
const [username, setUsername] = useState({});
const usernameSet = () => {
const name = {
firstname: 'Naruto',
familyname: 'Uzmaki'
}
setUsername(prevState => name);
}
return(
<React.Fragment>
<button onClick= {usernameSet}>
Store Object
</button>
{username.firstname} {username.familyname}
</React.Fragment>
)
}
export default ObjectState;
If you want to add an object to a pre-existing object.
import React from 'react';
import {useState, useEffect} from 'react';
const ObjectState= () => {
const [username, setUsername] = useState({village: 'Konoha'});
const usernameSet = () => {
setUsername((prevState) => {
const data = {
...prevState,
firstname: 'Naruto',
familyname: 'Uzmaki'
}
return data
});
}
return(
<React.Fragment>
<button onClick= {usernameSet}>
Store Object
</button>
{username.village} {username.firstname} {username.familyname}
</React.Fragment>
)
}
export default ObjectState;
P.S. : Naming the component 'Object' leads to an 'Maximum call stack size
exceeded error'. Other names are fine but for some reason 'Object' is
not.
Like the following is not ok.
const Object = () => {
// The above code
};
export default Object;
If anyone knows why or how to prevent it please add it to the comments.

Categories

Resources