Updates are affecting the states of multiple Contexts - reactjs

I am trying to save 2 sets of input values using React Context, compare them using JSON.stringify(context1) !== JSON.stringify(context2) and if they are not the same, the page triggers a "save" CTA.
What I am not understanding is that the input updates are affecting both contexts even though I am only updating the state of one. Not understanding why this is happening.
Also, happy to hear more elegant ways of doing this.
Below is some code that replicates the issue and here is a CodePen of it:
const { useContext, useEffect, useState } = React
const Context1 = React.createContext()
const Context2 = React.createContext()
function Child(props) {
const { passHandleChange } = props
const { context1, setContext1 } = useContext(Context1)
const onChange = (index, key, event) => {
passHandleChange(index, key, event)
}
const formatPeople = () => {
return context1.tempPersons.map((person, index) => {
return (
<div>
{ index }
<input value={context1.tempPersons[index].name} onChange={event => onChange(index, 'name', event)} />
<input value={context1.tempPersons[index].email} onChange={event => onChange(index, 'email', event)} />
</div>
)
})
}
return (
<div>
{ formatPeople() }
</div>
)
}
function App() {
const initProfile = {
isModified: false,
tempPersons: [
{
name: "Bob",
email: "bob#email.com"
},
{
name: "Jill",
email: "jill#email.com"
},
{
name: "John",
email: "john#email.com"
}
]
}
const handleChange = (index, key, event) => {
let value = event.target.value
let tempPersons = context1.tempPersons
tempPersons[index] = context1.tempPersons[index]
tempPersons[index][key] = value
setContext1(prevState => ({
...prevState,
tempPersons: tempPersons
}))
}
const [context1, setContext1] = useState(initProfile)
const [context2, setContext2] = useState(initProfile.tempPersons)
return (
<div>
<Context1.Provider value={ { context1, setContext1 } }>
<Context2.Provider value={ { context2, setContext2 } }>
<Child passHandleChange={ handleChange } />
<hr />
<pre>
context1
{ JSON.stringify(context1) }
</pre>
<pre>
context2
{ JSON.stringify(context2) }
</pre>
</Context2.Provider>
</Context1.Provider>
</div>
)
}
ReactDOM.render( <App />, document.getElementById("root") );

It’s because objects in JavaScript are passed by reference, so the two contexts are actually referring to the same array (tempPersons). When that array is modified through the reference in one context, the other context will also appear modified, because they’re referring to the same array in memory.
You can confirm this by running the following:
const sharedArray = ['a', 'b'];
const ref1 = sharedArray;
const ref2 = sharedArray;
ref1.push('c');
console.log(ref2.length);
// 3

Related

Component lose its state when re-renders

I have a sample where I have a list of answers and a paragraph that has gaps.
I can drag the answers from the list to the paragraph gaps. After the answer fills the gap, the answer will be removed from the list. 3 answers and 3 gaps, the answer list should be empty when i drag all of them to the gaps.
But whenever I filter the listData, the component re-renders and the listData gets reset. The list always remained 2 items no matter how hard I tried. What was wrong here?
My code as below, I also attached the code sandbox link, please have a look
App.js
import GapDropper from "./gapDropper";
import "./styles.css";
const config = {
id: "4",
sort: 3,
type: "gapDropper",
options: [
{
id: "from:drop_gap_1",
value: "hello"
},
{
id: "from:drop_gap_2",
value: "person"
},
{
id: "from:drop_gap_3",
value: "universe"
}
],
content: `<p>This is a paragraph. It is editable. Try to change this text. <input id="drop_gap_1" type="text"/> . The girl is beautiful <input id="drop_gap_2" type="text"/> I can resist her charm. Girl, tell me how <input id="drop_gap_3" type="text"/></p>`
};
export default function App() {
return (
<div className="App">
<GapDropper data={config} />
</div>
);
}
gapDropper.js
import { useEffect, useState } from "react";
import * as _ from "lodash";
import styles from "./gapDropper.module.css";
const DATA_KEY = "answerItem";
function HtmlViewer({ rawHtml }) {
return (
<div>
<div dangerouslySetInnerHTML={{ __html: rawHtml }} />
</div>
);
}
function AnwserList({ data }) {
function onDragStart(event, data) {
event.dataTransfer.setData(DATA_KEY, JSON.stringify(data));
}
return (
<div className={styles.dragOptionsWrapper}>
{data.map((item) => {
return (
<div
key={item.id}
className={styles.dragOption}
draggable
onDragStart={(event) => onDragStart(event, item)}
>
{item.value}
</div>
);
})}
</div>
);
}
function Content({ data, onAfterGapFilled }) {
const onDragOver = (event) => {
event.preventDefault();
};
const onDrop = (event) => {
const draggedData = event.dataTransfer.getData(DATA_KEY);
const gapElement = document.getElementById(event.target.id);
const objData = JSON.parse(draggedData);
gapElement.value = objData.value;
onAfterGapFilled(objData.id);
};
function attachOnChangeEventToGapElements() {
document.querySelectorAll("[id*='drop_gap']").forEach((element) => {
element.ondragover = onDragOver;
element.ondrop = onDrop;
});
}
useEffect(() => {
attachOnChangeEventToGapElements();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<HtmlViewer rawHtml={data} />
</div>
);
}
const GapDropper = ({ data }) => {
const [gaps, setGaps] = useState(() => {
return data.options;
});
function onAfterGapFilled(id) {
let clonedGaps = _.cloneDeep(gaps);
clonedGaps = clonedGaps.filter((g) => g.id !== id);
setGaps(clonedGaps);
}
return (
<div>
<div>
<AnwserList data={gaps} />
<Content
data={data.content}
onAfterGapFilled={(e) => onAfterGapFilled(e)}
/>
</div>
</div>
);
};
export default GapDropper;
Code sandbox
the problem is that you are not keeping on track which ids you already selected, so thats why the first time it goes right, and then the second one, the values just replace the last id.
Without changing a lot of your code, we can accomplish by tracking the ids inside a ref.
const GapDropper = ({ data }) => {
const [gaps, setGaps] = useState(() => {
return data.options;
});
const ids = useRef([])
function onAfterGapFilled(id) {
let clonedGaps = _.cloneDeep(gaps);
ids.current = [...ids.current, id]
clonedGaps = clonedGaps.filter((g) => !ids.current.includes(g.id));
setGaps(clonedGaps);
}
return (
<div>
<div>
<AnwserList data={gaps} />
<Content
data={data.content}
onAfterGapFilled={(e) => onAfterGapFilled(e)}
/>
</div>
</div>
);
};
Maybe there is a better solution but this one does the job

Why the list doesn't re-render after updating the store?

I started learning mobx and got stuck. Why when I change listItems, List doesn't re-render?
I have store:
export const listStore = () => {
return makeObservable(
{
listItems: [],
addItem(text) {
this.listItems.push(text);
}
},
{
listItems: observable,
addItem: action.bound
}
);
};
Component that adds text from input to store:
const store = listStore();
export const ListForm = observer(() => {
const [value, setValue] = useState();
return (
<>
<input type="text" onChange={e => setValue(e.target.value)} />
<button onClick={() => store.addItem(value)}>Add note</button>
</>
);
});
And I have a list component:
const store = listStore();
export const List = () => {
return (
<React.Fragment>
<ul>
<Observer>
{() => store.listItems.map(item => {
return <li key={item}>{item}</li>;
}
</Observer>
</ul>
<ListForm />
</React.Fragment>
);
};
I don't understand what's wrong. Looks like the list doesn't watch the store changing
codesandbox: https://codesandbox.io/s/ancient-firefly-lkh3e?file=/src/ListForm.jsx
You create 2 different instances of the store, they don't share data between. Just create one singleton instance, like that:
import { makeObservable, observable, action } from 'mobx';
const createListStore = () => {
return makeObservable(
{
listItems: [],
addItem(text) {
this.listItems.push(text);
}
},
{
listItems: observable,
addItem: action.bound
}
);
};
export const store = createListStore();
Working example

Preventing unnecessary rendering with React.js

I have a very basic application to test how to prevent unnecessary rendering, but I'm very confused as it is not working no matter what I try. Please take a look.
App.js
import { useState, useCallback } from "react";
import User from "./User";
let lastId = 0;
function App() {
const [users, setUsers] = useState([
{ id: 0, name: "Nicole Kidman", gender: "Female" },
]);
const handleUserChange = useCallback(
(e, userId) => {
const { name, value } = e.target;
const newUsers = [...users];
const index = newUsers.findIndex((user) => user.id === userId);
if (index >= 0) {
newUsers[index] = {
...newUsers[index],
[name]: value,
};
setUsers(newUsers);
}
},
[users]
);
const addNewUser = useCallback(() => {
let newUser = { id: ++lastId, name: "John Doe", gender: "Male" };
setUsers((prevUsers) => [...prevUsers, newUser]);
}, []);
return (
<div className="App">
<button onClick={addNewUser}>Add user</button>
<br />
{users.map((user) => (
<User key={user.id} user={user} handleUserChange={handleUserChange} />
))}
</div>
);
}
export default App;
User.js
import { useRef, memo } from "react";
const User = memo(({ user, handleUserChange }) => {
const renderNum = useRef(0);
return (
<div className="user">
<div> Rendered: {renderNum.current++} times</div>
<div>ID: {user.id}</div>
<div>
Name:{" "}
<input
name="name"
value={user.name}
onChange={(e) => handleUserChange(e, user.id)}
/>
</div>
<div>
Gender:{" "}
<input
name="gender"
value={user.gender}
onChange={(e) => handleUserChange(e, user.id)}
/>
</div>
<br />
</div>
);
});
export default User;
Why the useCallback and memo doesn't do the job here? How can I make it work, prevent rendering of other User components if another User component is changing(typing something in Input)?
Thank you.
useCallback and useMemo take a dependency array. If any of the values inside that array changes, React will re-create the memo-ized value/callback.
With this in mind, we see that your handleUsersChange useCallback is recreated every time the array users changes. Since you update the users state inside the callback, every time you call handleUsersChange, the callback is re-created, and therefore the child is re-rendered.
Solution:
Don't put users in the dependency array. You can instead access the users value inside the handleUsersChange callback by providing a callback to setUsers functions, like so:
const handleUserChange = useCallback(
(e, userId) => {
const { name, value } = e.target;
setUsers((oldUsers) => {
const newUsers = [...oldUsers];
const index = newUsers.findIndex((user) => user.id === userId);
if (index >= 0) {
newUsers[index] = {
...newUsers[index],
[name]: value,
};
return newUsers;
}
return oldUsers;
})
},
[]
);

ReactJs managing an array of stateful components

I have a parent component that I want to use to dynamically add and remove an stateful component, but something weird is happening.
This is my parent component and how I control my list of FilterBlock
import React from 'react'
import FilterBlock from './components/FilterBlock'
export default function List() {
const [filterBlockList, setFilterList] = useState([FilterBlock])
const addFilterBlock = () => {
setFilterList([...filterBlockList, FilterBlock])
}
const onDeleteFilterBlock = (index) => {
const filteredList = filterBlockList.filter((block, i) => i !== index);
setFilterList(filteredList)
}
return (
<div>
{
filterBlockList.map((FilterBlock, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
/>
))
}
<button onClick={addFilterBlock}></button>
</div>
)
}
You can assume FilterBlock is a stateful react hooks component.
My issue is whenever I trigger the onDeleteFilter from inside any of the added components, only the last pushed FilterBlock gets removed from the list even though I remove it based on its index on the list.
Please check my example here:
https://jsfiddle.net/emad2710/wzh5Lqj9/10/
The problem is you're storing component function FilterBlock to the state and mutate the parent state inside a child component.
You may know that mutating state directly will cause unexpected bugs. And you're mutating states here:
/* Child component */
function FilterBlock(props) {
const [value, setValue] = React.useState('')
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
]
const onInputChange = (e) => {
// child state has been changed, but parent state
// does not know about it
// thus, parent state has been mutated
setValue(e.target.value)
}
return (
<div>
<button onClick={props.onDeleteFilter}>remove</button>
<input value={value} onChange={onInputChange}></input>
</div>
)
}
/* Parent component */
const addFilterBlock = () => {
setFilterList([...filterBlockList, FilterBlock])
}
filterBlockList.map((FilterBlock, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
/>
))
When a FilterBlock value changes, FilterBlock will store its state somewhere List component cannot control. That means filterBlockList state has been mutated. This leads to unpredictable bugs like the above problem.
To overcome this problem, you need to manage the whole state in parent component:
const {useCallback, useState} = React;
function FilterBlock(props) {
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
]
return (
<div>
<button onClick={props.onDeleteFilter}>remove</button>
<input value={props.value} onChange={props.onChange}></input>
</div>
)
}
function App() {
const [values, setValues] = useState(['strawberry']);
const addFilterBlock = useCallback(() => {
setValues(values.concat(''));
}, [values]);
const onDeleteFilterBlock = useCallback((index) => {
setValues(values.filter((v, i) => i !== index));
}, [values]);
const setFilterValue = useCallback((index, value) => {
const newValues = [...values];
newValues[index] = value;
setValues(newValues);
}, [values]);
return (
<div>
{
values.map((value, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
value={value}
onChange={(e) => setFilterValue(index, e.target.value)}
/>
))
}
<button onClick={addFilterBlock}>add</button>
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
JSFiddle

Cannot access ___ before initialization reactjs useState useTracker Subscriptions Form state meteor

I have a form that takes its state from a react useState hook, that hooks default value I would like to come from a useTracker call, I am using pub sub in Meteor to do this. I get a error Cannot access '' before initialization I know it has something to do with the lead not being ready yet and returning undefined and the hook not being able to use that, at least I think so. But I am not sure how to solve that.
Here is my code thus far
import React, { useState } from "react";
import Dasboard from "./Dashboard";
import { Container } from "../styles/Main";
import { LeadsCollection } from "../../api/LeadsCollection";
import { LeadWalkin } from "../leads/LeadWalkin";
import { useTracker } from "meteor/react-meteor-data";
const Walkin = ({ params }) => {
const [email, setEmail] = useState(leads.email);
const handleSubmit = (e) => {
e.preventDefault();
if (!email) return;
Meteor.call("leads.update", email, function (error, result) {
console.log(result);
console.log(error);
});
setEmail("");
};
const { leads, isLoading } = useTracker(() => {
const noDataAvailable = { leads: [] };
if (!Meteor.user()) {
return noDataAvailable;
}
const handler = Meteor.subscribe("leads");
if (!handler.ready()) {
return { ...noDataAvailable, isLoading: true };
}
const leads = LeadsCollection.findOne({ _id: params._id });
return { leads };
});
console.log(leads);
//console.log(params._id);
const deleteLead = ({ _id }) => {
Meteor.call("leads.remove", _id);
window.location.pathname = `/walkin`;
};
return (
<Container>
<Dasboard />
<main className="split">
<div>
<h1>Edit a lead below</h1>
</div>
{isLoading ? (
<div className="loading">loading...</div>
) : (
<>
<LeadWalkin
key={params._id}
lead={leads}
onDeleteClick={deleteLead}
/>
<form className="lead-form" onSubmit={handleSubmit}>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Type to edit lead"
/>
<button type="submit">Edit Lead</button>
</form>
</>
)}
</main>
</Container>
);
};
export default Walkin;
It should work if you change the order of these two hooks, but it's probably better to break this into two components so that you can wait until your subscription is ready before you try to use leads.email as default value. It's not possible to branch out ('return loading`) in between hooks, because React doesn't like it when the number of hooks it finds in a component change in-between re-renderings.
const Walkin = ({ params }) => {
const { leads, isLoading } = useTracker(() => {
const noDataAvailable = { leads: [] };
if (!Meteor.user()) {
return noDataAvailable;
}
const handler = Meteor.subscribe("leads");
if (!handler.ready()) {
return { ...noDataAvailable, isLoading: true };
}
const leads = LeadsCollection.findOne({ _id: params._id });
return { leads };
});
if (isLoading || !leads) {
return <div>loading..</div>;
} else {
return <SubWalkin params=params leads=leads />;
}
};
const SubWalkin = ({ params, leads }) => {
const [email, setEmail] = useState(leads.email);
...
};

Resources