React storing Ref elements in Redux - reactjs

How would you go about storing Ref elements in Redux and would you do it at all?
I have a component containing some form elements where I need to store the state of which field in the form the user had selected if they leave the page and come back.
I tried registering each input field in Redux like so (I'm using the <InputGroup> component from Blueprint.js):
<InputGroup
inputRef={(ref) => { dispatch(addRefToState(ref)) }}
...more props...
/>
That resulted in a circular JSON reference error since Redux is serializing the ref element to JSON in order to save it to localStorage. I then tried "safe" stringifying the object with a snippet I found here on Stackoverflow, removing any circular references before turning the object into JSON. That sort of works, but the Ref elements are still so huge that 3-5 refs stored in state turns into a 3MB localStorage and my browser starts being painfully slow. Further, I'm concerned whether I can actually use that Ref object to reference my components, know that I essentially modified the Ref object. I've not tried yet, because performance is so poor with the stringified objects.
I'm contemplating abandoning "the React way" and just adding unique IDs on each component, storing those in Redux and iterating over the DOM with document.querySelector to focus the right element when the page is loaded. But it feels like a hack. How would you go about doing this?

I am not sure if I would use them for that purpose but It would not be among the first ways to do that.
It is perfectly fine to have a React state to store a unique identifier of focused form element. Every form element, or any element in general, can have a unique identifier which can just be a string. You can keep them in your app's redux store in any persistence like web storage.
While you navigate away you can commit that state to your redux store or to persistence, by using a React effect.
const [lastFocusedElementId, setLastFocusedElementId] = useState();
useEffect(() => {
// here you can get last focused element id from props, or from directly storage etc, previously if any
if(props.lastFocusedElID) {
setLastFocusedElementId(props.lastFocusedElID);
}
// here in return you commit last focused id
return saveLastFocusedElementID(lastFocusedElementId) // an action creator that saves to the redux store before every rerender of component and before unmount
}, [props.lastFocusedElID]);
Alternatively
const [lastFocusedElementId, setLastFocusedElementId] = useState();
useEffect(() => {
const lastFocusedElID = window.localStorage.getItem(lastFocusedElementId);
if (lastFocusedElID) {
setLastFocusedElementId(lastFocusedElID);
}
return window.localStorage.setItem('lastFocusedElementId', lastFocusedElementId);
}, []);
Not to mention you need to use onFocus on form elements you want to set the last focused element ID. Id can be on an attribute of your choice, it can be id attribute, or you can employ data-* attributes. I showed id and data-id here.
<input onFocus={e => setLastFocusedElementId(e.target.id)} />
<input onFocus={e => setLastFocusedElementId(e.dataset.id)} />
Also needed a way to focus the last focused element with the data from your choice of source when you reopen the page with that form elements. You can add autoFocus attribute every element like
<input autoFocus={"elementID"===lastFocusedElementId} />
Lastly if your user leave the form page without focusing any element you might like to set the id to a base value like empty string or so. In that case you need to use blur events with onBlur handler(s).

Related

React component getting wrong value from a wrong field field in the state object when I use "defaultValue" to assign values

My code is as follows, and you can see how it works in https://codepen.io/rongeegee/pen/BaVJjGO:
const { useState } = React;
const Counter = () => {
const [data, setData] = useState({
displayData: "data_one",
data_one: {
text: ""
},
data_two:{
text:""
}
})
function handleOnChange(event){
event.preventDefault();
const new_data = {...data};
if (event.target.name == "displayData"){
new_data.displayData = event.target.value;
setData(new_data);
}
else{
new_data[event.target.name]["text"] = event.target.value;]
setData(new_data);
}
}
return (
<div>
<form onChange={handleOnChange}>
<select name="displayData" value={data.displayData}>
<option value="data_one">data_one</option>
<option value="data_two">data_two</option>
</select>
<br/>
{
data.displayData == "data_one"
?
<>data One: <input name="data_one" defaultValue={data.data_one.text} /></>
:
<>data two: <input name="data_two" defaultValue={data.data_two.text} /></>
}
</form>
</div>
)
}
ReactDOM.render(<Counter />, document.getElementById('app'))
If I type something in the input of data_one, toggle between the values between "data_one" and "data_two", the data_two input field will have the same value inside. If I change the value in data_one toggle the dropdown to "data_one", data_one will have the same value again.
This shouldn't happen since data_one input uses the value of the text field in data_one field in the data state while data_two input uses the one in data_two field. One should not take the value from another field in the state.
React has a way to determine if/which elements/components have changed and which haven't. This is neccesary, because DOM manipulation is expensive, so we should try to limit it as much as possible. That's why React has a way to determine what changed between rerenders and what didn't; and only changes what changed in the DOM.
So if we in your case swith from dataOne to dataTwo, React goes something like: "Oh nice input element stays input element. Nice I don't have to completely destroy that DOM node and render it rom scratch, I can just check what changed and change that. Let's see: The name has changed ... so let's change that, but apart from that everything stayed the same." (This means your input element won't get destroyed and the other one initialy rendered, but the one input element get's a name change and React calls it a day - and since default Value only works on initial creation of an element/DOM node, it won't be shown)
The way React rerenders stuff and compares/modifies the DOM is quite complicated. For futher information I can recomend the following video: https://youtu.be/i793Qm6kv3U (It really helped me understand the whole React Render process way better).
A possible fix to your problem, would be to give each input element a key. So your input elements could look something like:
<input key="1" name="data_one" defaultValue={data.data_one.text} />
<input key="2" name="data_two" defaultValue={data.data_two.text} />
So yeah, the fix is fairly easy; understanding the reason to why we need this fix however is everything but easy.
regarding your comment (my answer would be to long for a comment, and formatting is nicer in an answer)
nope you aren't changing state on input-field change ... you are changing/manipulating the state variable data ... but you are not updating your state. Hence no rerender gets triggered, and in case of a rerender triggered by something else the data will be lost. So you aren't actually changing state.
Changes to your state can only be made by calling the callback Funcnction provided by the useState-Hook. (In your case the callback provided by the useState-Hook is setData. In your else statement you are not calling the provided callback, just manipulating the data object (since you shallow clone and therefor manipulate the original data object)
When changing state ALWAYS use the provided callback const [stateVariable, thisIsTheCallback] = useState(initVal). If you don't use this callback and just manipulate the stateVariable you will sooner or later run into problems (and debugging that issue is particularly tedious, since it seems like you are changing state (because the stateVariable changes) but you aren't changing state since only the callback can do this)
In place of defaultValue attribute to your <input/> replace that with value attribute and everything will work.
And to know why defaultValue won't get updated after state change, read the first answer by #Lord-JulianXLII

Reusing components in react and telling the component which key: value of a state to change

I'm having a little bit of difficulty figuring out how to reuse components in many places.
Obviously the reason why react is so good is because you can create common components and reuse them across many different pages, or just multiple times in the same page.
I have this component for displaying a dropdown that a user can use to select a new value, and change their settings. This component is reused in many different places on my website.
<ChannelSelect
channelID={
saveEnabled.saveEnabled.newSettings.verificationPanel.channelID
}
settings={
saveEnabled.saveEnabled.newSettings.verificationPanel.channelID
}
/>
What I am struggling with is telling this component which key: value in the state (object) to change. This is because simply passing in the code below, doesn't actually allow me to change the state of this specific key.
settings={saveEnabled.saveEnabled.newSettings.verificationPanel.channelID}
In my select menus onChange event I have the code snippet below. However, this doesn't actually allow me to change the specific value declare, it would actually just overwrite the whole object.
const { saveEnabled, setSaveEnabled } = useContext(SaveContext);
const onChange = (event) => {
props.settings = "newValue"
setSaveEnabled(props.settings)
Any idea how I can do this?

How do I use React refs in a map for functional components?

I have a map function which renders a list of divs and these divs are based on the components state, so it is dynamic. I need to put a ref on each of these items on the list so that the user can basically press up and press down and traverse the list starting from the top.
Something like this:
const func = () => {
const item = items.map((i, index) => {
return (
<div ref={index}>{i.name}</div>
)
});
}
This has been difficult since it seems like refs are not like state where they can change based on dynamic data. Its initialized once and set to a constant. But I don't know what to initialize it as until this list is rendered and I can't call the useRef hook in a function of course so what is the solution here?
EDIT:
Ok so, its seems there needs to be some more context around this question:
I have a dropdown which also allows the user to search to filter out the list of items. I want to make an enhancement to it so that when the user searches it focuses on the first element and allows the use of arrow keys to traverse the list of elements.
I got stuck when I tried to focus on the first element because the first element doesn't exist until the list is rendered. This list changes dynamically based on what the user types in. So how do you focus on the first element of the list if the list is always changing as the user types?
This can be done without refs, so it should probably be done without refs.
You can put the index of the currently focused element into state. Whenever the displayed data changes, reset the index to 0:
const [focusedElementIndex, setFocusedElementIndex] = useState(0);
useEffect(() => {
setFocusedElementIndex(0);
}, [items]);
// using this approach,
// make sure items only gets a new reference when the length changes
When rendering, if you need to know the focused index to change element style, for example, just compare the index against the state value:
<div class={index === focusedElementIndex ? 'focused' : ''}
And have a keydown listener that increments or decrements focusedElementIndex when the appropriate key is pressed.

How can I update an object in a nested array when an input value changes?

I have a list of articles, each with some number of exercises, and each exercise has a duration property. The duration property is editable, but I can't seem to figure out how to update this so it takes effect in my original array.
I've setup a basic example in CodeSandbox.
Expected behavior is when changing one of the input fields, it should update in my articles array (which is located in App.js)
I'm trying to do: onChange={(e) => setDuration(e.target.value)}
As I see it, the problem is that React doesn't like if you change the current state, so I would have to create a new state... So this "new" duration should somehow propagate to App.js maybe? Or is the solution to have a method in App.js that you pass down all components to update the original array?
Any advice would be greatly appreciated :)
changing the state in the app itself doesn't change the object the app received.
you'll need to manually change the object
try adding:
const onChange = (e)=>{
exercise.duration = e.target.value;
setDuration(e.target.value)
}
and change your
onChange={(e) => setDuration(e.target.value)}
to:
onChange={onChange}
NOTE: this wont cause the parent to rerender.

ReactJS issue on my test app

So, I've been working through my first ReactJS app. Just a simple form where you type in a movie name and it fetches the data from IMDB and adds them as a module on the page. That's all working fine.
However each movie module also had a remove button which should remove that particular module and trigger a re-render. That's not working great as no matter which button you click it always removes the last movie module added rather than the one you're clicking on.
App:
http://lukeharrison.net/react/
Github codebase:
https://github.com/WebDevLuke/React-Movies
I'm just wondering if anybody can spot the reasoning behind this?
Cheers!
Just a hunch, but you should use a unique key, not just the index of the map function. This way React will understand that the movies are identified not by some iterating index, but an actual value, and that will probably solve your issue.
var movies = this.state.movies.map(function(movie, index){
return (
<Movie key={movie} useKey={index} removeMovieFunction={component.removeMovie} search={movie} toggleError={component.toggleError} />
);
});
This is because React re-evaluates your properties, sees that nothing has changed, and just removes the last <Movie /> from the list. Each Movie's componentDidMount function never runs more than once, and the state of Movie 1, Movie 2 and Movie 3 persists. So even if you supply search={movie} it doesn't do anything, because this.props.search is only used in componentDidMount.
I'm not exactly sure why it isn't rendering correctly as the dataset looks fine.
Looking at the code, I would change your remove function to this...
var index = this.state.movies.indexOf(movieToRemove);
console.log(this.state.movies);
if (index > -1) {
this.state.movies.splice(index, 1);
}
console.log(this.state.movies);
this.setState(this.state.movies);
My assumption is that, the state isn't being updated correctly. Whenever updating state, you should always use setState (unless the convention changed and I wasn't aware).
Also, you shouldn't need to explicitly call forceUpdate. Once setState is called, React will automatically do what it needs to and rerender with the new state.
State should be unidirectional (passed top down) from your top level component (known as a container). In this instance, you have state in your top level component for search strings and then you load individual movie data from within the "Movie" component itself via the IMDB API.
You should refactor your code to handle all state at the top level container and only pass the complete movie data to the dumb "Movie" component. all it should care about is rendering what you pass in it's props and not about getting it's own data.

Resources