I am having an issue with keeping track of collection state updates in ReactJS
I have model called Ticket
export default class Ticket {
numbers?: number[];
constructor(numbers?: number[]) {
if (numbers == undefined) {
numbers = []
}
this.numbers = numbers
}
}
Now in my Home Component I have the following
const [tickets, setTickets] = useState<Ticket[]>([]);
const [activeTicket, setActiveTicket] = useState<Ticket>(new Ticket());
Now, I have a function as below
const addMoreTickets = () => {
let newTicket = new Ticket();
setTickets([...tickets, newTicket]);
setActiveTicket(tickets[tickets.length-1]); //This doesn't work, it still holds the old ticket instead of the newly added ticket
}
Is there a work around to this?
Related
I am trying to apply the DDD principles of aggregate roots and always-consistent domain models to my frontend.
I have a fairly complex entity and I am designing a UI to manipulate it. I am using that complex entity as a state variable. The problem I am having is that any time I update the entity, I have to re-create the entire object so that React detects a state change. However, doing this causes everything on the page to re-render since it's all dependent on this single entity for state.
Here are the entities. It's a school with a list of students. The user will first select a student and then edit it.
class School {
public SelectedStudentId: string;
private _students: Array<Student> = [];
constructor(students: Array<Student>) {
this._students = students;
}
public get Students() {
return this._students.sort(x => x.DisplayOrder);
}
public GetSelectedStudent(): Student {
const res = this._students.find(x => x.Id === this.SelectedStudentId);
if (!res) throw new Error("Could not find selected student")
return res;
}
public SetSelectedStudent(studentId: string) {
if (this._students.some(x => x.Id === studentId)) {
this.SelectedStudentId = studentId;
} else {
throw new Error(`No student with ID ${studentId} exists`);
}
}
public UpdateSelectedStudent(newStudent: Student) {
// perform checks on the validity of newStudent, then:
this._students = [
...this._students.filter(x => x.Id === this.SelectedStudentId),
newStudent
]
}
}
class Student {
public Id: string;
public Name: string;
public DisplayOrder: number;
constructor(name: string, displayOrder: number, id?: string) {
this.Name = name;
this.DisplayOrder = displayOrder;
this.Id = id ?? uuid();
}
}
I am then using the School object as state in the following React component:
export default function Home() {
const [school, setSchool] = useState<School>();
useEffect(() => {
const data = fetchFromSomeApi();
setSchool(data);
}, []);
const setSelectedStudent = (studentId: string) => {
const newSchool: School = cloneDeep(school);
newSchool.SetSelectedStudent(studentId);
setSchool(newSchool);
}
const updateStudent = (newStudent: Student) => {
const newSchool: School = cloneDeep(school);
newSchool.UpdateSelectedStudent(newStudent);
setSchool(newSchool);
}
return (
<div>
{school === undefined ? (
<div>Loading...</div>
) : (
<>
<StudentSelectList
students={school.Students}
setSelectedStudent={setSelectedStudent}
/>
<StudentEditor
student={school.GetSelectedStudent()}
updateStudent={updateStudent}
/>
</>
)}
</div>
)
}
The problem lies in the React component's setSelectedStudent and updateStudent functions. They clone the school state variable - which practically everything on the page is dependent - which then causes every single component to re-render. It's laggy. I don't think using React.memo() or useCallback() will help here since they will be dependent on the school object.
This is making me wonder if having a large, DDD-style aggregate root in React (at least as state) is a good idea at all. I can see very few examples online of people doing this. It would work brilliantly if it weren't for the performance issues.
Is there a solution that will allow me to use the School class? Maybe something with useRef?
I'm working on my first React project and I have the following problem.
How I want my code to work:
I add Items into an array accessible by context (context.items)
I want to run a useEffect function in a component, where the context.items are displayed, whenever the value changes
What I tried:
Listing the context (both context and context.items) as a dependency in the useEffect
this resulted in the component not updating when the values changed
Listing the context.items.length
this resulted in the component updating when the length of the array changed however, not when the values of individual items changed.
wraping the context in Object.values(context)
result was exactly what I wanted, except React is now Complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
Do you know any way to fix this React warning or a different way of running useEffect on context value changing?
Well, didn't want to add code hoping it would be some simple error on my side, but even with some answers I still wasn't able to fix this, so here it is, reduced in hope of simplifying.
Context component:
const NewOrder = createContext({
orderItems: [{
itemId: "",
name: "",
amount: 0,
more:[""]
}],
addOrderItem: (newOItem: OrderItem) => {},
removeOrderItem: (oItemId: string) => {},
removeAllOrderItems: () => {},
});
export const NewOrderProvider: React.FC = (props) => {
// state
const [orderList, setOrderList] = useState<OrderItem[]>([]);
const context = {
orderItems: orderList,
addOrderItem: addOItemHandler,
removeOrderItem: removeOItemHandler,
removeAllOrderItems: removeAllOItemsHandler,
};
// handlers
function addOItemHandler(newOItem: OrderItem) {
setOrderList((prevOrderList: OrderItem[]) => {
prevOrderList.unshift(newOItem);
return prevOrderList;
});
}
function removeOItemHandler(oItemId: string) {
setOrderList((prevOrderList: OrderItem[]) => {
const itemToDeleteIndex = prevOrderList.findIndex((item: OrderItem) => item.itemId === oItemId);
console.log(itemToDeleteIndex);
prevOrderList.splice(itemToDeleteIndex, 1);
return prevOrderList;
});
}
function removeAllOItemsHandler() {
setOrderList([]);
}
return <NewOrder.Provider value={context}>{props.children}</NewOrder.Provider>;
};
export default NewOrder;
the component (a modal actually) displaying the data:
const OrderMenu: React.FC<{ isOpen: boolean; hideModal: Function }> = (
props
) => {
const NewOrderContext = useContext(NewOrder);
useEffect(() => {
if (NewOrderContext.orderItems.length > 0) {
const oItems: JSX.Element[] = [];
NewOrderContext.orderItems.forEach((item) => {
const fullItem = {
itemId:item.itemId,
name: item.name,
amount: item.amount,
more: item.more,
};
oItems.push(
<OItem item={fullItem} editItem={() => editItem(item.itemId)} key={item.itemId} />
);
});
setContent(<div>{oItems}</div>);
} else {
exit();
}
}, [NewOrderContext.orderItems.length, props.isOpen]);
some comments to the code:
it's actually done in Type Script, that involves some extra syntax
-content (and set Content)is a state which is then part of return value so some parts can be set dynamically
-exit is a function closing the modal, also why props.is Open is included
with this .length extension the modal displays changes when i remove an item from the list, however, not when I modify it not changeing the length of the orderItems,but only values of one of the objects inside of it.
as i mentioned before, i found some answers where they say i should set the dependency like this: ...Object.values(<contextVariable>) which technically works, but results in react complaining that *The final argument passed to useEffect changed size between renders. The order and size of this array must remain constant. *
the values displayed change to correct values when i close and reopen the modal, changing props.isOpen indicating that the problem lies in the context dependency
You can start by creating your app context as below, I will be using an example of a shopping cart
import * as React from "react"
const AppContext = React.createContext({
cart:[]
});
const AppContextProvider = (props) => {
const [cart,setCart] = React.useState([])
const addCartItem = (newItem)=>{
let updatedCart = [...cart];
updatedCart.push(newItem)
setCart(updatedCart)
}
return <AppContext.Provider value={{
cart
}}>{props.children}</AppContext.Provider>;
};
const useAppContext = () => React.useContext(AppContext);
export { AppContextProvider, useAppContext };
Then you consume the app context anywhere in the app as below, whenever the length of the cart changes you be notified in the shopping cart
import * as React from "react";
import { useAppContext } from "../../context/app,context";
const ShoppingCart: React.FC = () => {
const appContext = useAppContext();
React.useEffect(() => {
console.log(appContext.cart.length);
}, [appContext.cart]);
return <div>{appContext.cart.length}</div>;
};
export default ShoppingCart;
You can try passing the context variable to useEffect dependency array and inside useEffect body perform a check to see if the value is not null for example.
I am a beginner in React, and I am missing something...
I have a simple "shopping list" app. I have an array of "product" object (it just has name and amount properties), the user just add products to the list,
and when the user adds a product that already exists in the list, the amount is increased.
So far so good...
So the problem is that I am trying to save the list every time the componentDidUpdate, I am checking if the list has changed (I am trying to compare it with the list from the previous state), but when I update product amount, the list from the previous state is the same like the list from the current state (when I add a new product to the list the previous state is not the same).
Why???
This is the product class:
export default class product {
amount = 0;
name = undefined;
constructor (name, count) {
this.name = name;
if (count)
this.amount = count;
else
this.amount++;
this.addOne = this.addOne.bind(this);
}
addOne = () => {
console.log(`${this.name} product added one`);
this.amount++;
}
}
The adding product function & handle duplication function:
addProduct = (product) => {
console.log('Add Product - form', product);
const duplicatedIndex = this.getDuplicationIndex(product);
if (duplicatedIndex >= 0) {
this.handleDuplication(duplicatedIndex);
}
else { //there is no duplication - just add new product to the list
this.setState((prevState) => ({ productsList: prevState.productsList.concat([ product ]) }));
}
}
handleDuplication = (index) => {
let duplicatedProduct = this.state.productsList[ index ];
duplicatedProduct.addOne();
const newList = this.state.productsList;
this.setState({
productsList: newList,
});
}
And the componentDidUpdate:
componentDidUpdate = (prevProps, prevState) => {
console.log('prev', prevState);
if (!utils.compareLists(this.state.productsList, prevState.productsList)) {
console.log("saving data");
const json = JSON.stringify(this.state.productsList);
localStorage.setItem('productsList', json);
}
}
The compare lists function:
static compareLists(list1, list2){
console.log(`list1`, list1);
console.log('list2', list2);
if(list1.length !== list2.length)
return false;
for(let i = 0; i < list1.length; i++){
if(list1[i] !== list2[i]){
return false;
}
}
return true;
}
So, why does list2 (from the prevState) is the same like list1 (from the current state) when some object is updated but they are different when some object is added to list1
For starters --- the compare function is completely unnecessary so long as we handle the state properly.
There are a few issues with state:
(1) Mutating state directly in handleDuplication and addProduct...For more on why this is important read Dave Ceddia's Why Not to Mutate State Directly.
(1.1) React will not do a deep comparison on attribute values for nested properties in an object. When updating the amount and whatnot make sure setting state with the syntax below (again, immutable, make a copy using spread below):
this.setState(prevState => ({
person: {
...prevState.name,
firstName: "Tom",
secondName: "Jerry"
}
}));
Example above taken from: https://www.rockyourcode.com/react-set-state-with-prev-state-and-object-spread-operator/
Below is from your snippet, directly mutating old state. However on further inspection I don't see any need for this given that it's setting the state with whatever the old state list was...Perhaps accidental? In any case could be part of the culprit:
const newList = this.state.productsList;
this.setState({
productsList: newList,
});
}
const newList = this.state.productsList.slice();
this.setState({
productsList: newList,
});
}
Another Potential Change for Product Class:
(1) This makes sure the previous amount is not lost due to adding say two products and then another two of the same product later on.
this.name = name;
if (count)
this.amount = **this.amount+=count;**
this.addOne = this.addOne.bind(this);
}
addOne = () => {
console.log(`${this.name} product added one`);
**this.amount+=1**;
}
Hope that helps --- Cheers.
I'm still relatively new to React so this may not be an issue or one that's solvable. I put together a very contrived (don't judge :P) experiment using Mobx and React (hooks) here.
What I am wondering is if there is any way to avoid the need to re-render the parent (App) for each item that is added to an array marked observable despite the items being added via concat.
For example:
export class MyModel {
constructor() {
this.messages = [];
}
addMemo(msg: string, done = false) {
const memos: Memo[] = [];
for (let i = 0; i < 10; i++) {
const memo = new Memo(`${msg}-${i}`);
memo.letsDance();
memos.push(memo);
}
this.messages = this.messages.concat(memos);
return memos.shift();
}
}
decorate(MyModel, {
messages: observable,
addMemo: action
});
Causes React to render the consumer 10 times:
const App = () => {
// ...
const myModel = useObservable(new MyModel());
// ...
return useObserver(() => (
<div>
{/* ... */}
</div>
));
};
https://codesandbox.io/s/white-tdd-jh42r
Your async letsDance() makes react rerender 10 times (1 for each item which 'dances') check an example here which comments out that part: https://codesandbox.io/s/interesting-mountain-wdtdg
Recently I contemplated the idea of having central state management in my React apps without using Redux or Mobx, instead opting to create something similar to the application class in Android. In any event, I implemented something similar to this:
Create a store folder and a file called store.js in it whose contents are:
// State
let state = {
users: {},
value: 0
};
// Stores references to component functions
let triggers = [];
// Subscription Methods
export const subscribe = trigger => {
triggers.push(trigger);
trigger();
}
export const unsubscribe = trigger => {
let pos = -1;
for (let i in triggers) {
if (triggers[i]===trigger) {
pos = i;
break;
}
}
if (pos!==-1) {
triggers.splice(pos, 1);
}
}
// Trigger Methods
let triggerAll = () => {
for (let trigger of triggers) {
trigger();
}
}
// State Interaction Methods
export const setUser = (name, description) => {
state.users[name] = description;
triggerAll();
}
export const removeUser = name => {
if (name in state.users) {
delete state.users[name];
}
triggerAll();
}
export const getAllUsers = () => {
return state.users;
}
export const getUser = name => {
if (!(name in state.users)) {
return null;
}
return state.users[name];
}
export const getValue = () => {
return state.value;
}
export const setValue = value => {
state.value = value;
triggerAll();
}
And connecting to this store in the following manner:
// External Modules
import React, { Component } from 'react';
import {Box, Text, Heading} from 'grommet';
// Store
import {subscribe, unsubscribe, getAllUsers} from '../../store/store';
class Users extends Component {
state = {
users: []
}
componentDidMount() {
subscribe(this.trigger); // push the trigger when the component mounts
}
componentWillUnmount() {
unsubscribe(this.trigger); // remove the trigger when the component is about to unmount
}
// function that gets triggered whenever state in store.js changes
trigger = () => {
let Users = getAllUsers();
let users = [];
for (let user in Users) {
users.push({
name: user,
description: Users[user]
});
}
this.setState({users});
}
render() {
return <Box align="center">
{this.state.users.map(user => {
return <Box
style={{cursor: "pointer"}}
width="500px"
background={{color: "#EBE7F3"}}
key={user.name}
round
pad="medium"
margin="medium"
onClick={() => this.props.history.push("/users/" + user.name)}>
<Heading margin={{top: "xsmall", left: "xsmall", right: "xsmall", bottom: "xsmall"}}>{user.name}</Heading>
<Text>{user.description}</Text>
</Box>
})}
</Box>;
}
}
export default Users;
Note. I've tested this pattern on a website and it works. Check it out here. And I apologize I am trying to keep the question concise for stackoverflow, I've provided a more detailed explanation of the pattern's implementation here
But anyway, my main question, what could be the possible reasons not to use this, since I assume if it was this simple, people wouldn't be using Redux or Mobx. Thanks in advance.
That's what Redux and MobX basically do, you are wrong in thinking that at their core concept they are much different. Their size and complexity came as a result of their effort to neutralize bugs and adapt to a vast variety of application cases. That's it. Although they might be approaching the task from different angles, but the central concept is just that. Maybe you should familiarize your self with what they actually do underneath.
Btw, you do not need to store redundant state in your component, if all you need is to trigger the update. You can just call forceUpdate() directly:
// function that gets triggered whenever state in store.js changes
trigger = () => {
this.forceUpdate();
}
That's similar to what Redux and MobX bindings for react do under the hood.