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?
Related
My question is: is the below pattern a good idea in React or no? I come from Java world where this type of code is standard. However, I've ran into several things that, while being a good idea in Java, are NOT a good idea in ReactJS. So I want to make sure that this type of code structure does not have weird memory leaks or hidden side-effects in the react world.
Some notes on below code: I'm only putting everything in the same file for brevity purposes. In real life, the react component the interface and the class would all be in their own source files.
What I'm trying to do: 1) Separate the display logic from data access logic so that my display classes are not married to a specific implementation of talking to a database. 2) Separating DAO stuff into interface + class so that I can later use a different type of database by replacing the class implementaton of the same DAO and won't need to touch much of the rest of the code.
so, A) Is this a good idea in React? B) What sort of things should I watch out for with this type of design? and C) Are there better patterns in React for this that I'm not aware of?
Thanks!
import { useState, useEffect } from 'react';
interface Dao {
getThing: (id: string) => Promise<string>
}
class DaoSpecificImpl implements Dao {
tableName: string;
constructor(tableName: string) {
this.tableName = tableName;
}
getThing = async (id: string) => {
// use a specific database like firebase to
// get data from tabled called tablename
return "herp";
}
}
const dao: Dao = new DaoSpecificImpl("thingies");
const Display: React.FC = () => {
const [thing, setThing] = useState("derp");
useEffect(() => {
dao.getThing("123").then((newThing) =>
setThing(newThing));
});
return (
<div>{thing}</div>
)
}
export default Display;
https://codesandbox.io/s/competent-taussig-g948n?file=/src/App.tsx
The DaoSpecificImpl approach works however I would change your component to use a React hook:
export const useDAO = (initialId = "123") => {
const [thing, setThing] = useState("derp");
const [id, setId] = useState(initialId);
useEffect(() => {
const fetchThing = async () => {
try{
const data = await dao.getThing(id);
setThing(data);
}catch(e){
// Handle errors...
}
}
fetchThing();
}, [id]);
return {thing, setId};
}
using the hook in your component:
const Display = () => {
const {thing, setId} = useDao("123"); // If you don't specify initialId it'll be "123"
return <button onClick={() => setId("234")}>{thing}</button> // Pressing the button will update "thing"
}
Side note: You could also use a HOC:
const withDAO = (WrappedComponent, initialId = "123") => {
.... data logic...
return (props) => <WrappedComponent {...props} thing={thing} setId={setId}/>
};
export default withDAO;
E.g. using the HOC to wrap a component:
export default withDao(Display); // If you don't specify initialId it'll be "123"
I'm using custom hooks for a component, and the custom hook uses a custom context. Consider
/* assume FooContext has { state: FooState, dispatch: () => any } */
const useFoo = () => {
const { state, dispatch } = useContext(FooContextContext)
return {apiCallable : () => apiCall(state) }
}
const Foo = () => {
const { apiCallable } = useFoo()
return (
<Button onClick={apiCallable}/>
)
}
Lots of components will be making changes to FooState from other components (form inputs, etc.). It looks to me like Foo uses useFoo, which uses state from FooStateContext. Does this mean every change to FooContext will re-render the Foo component? It only needs to make use of state when someone clicks the button but never otherwise. Seems wasteful.
I was thinking useCallback is specifically for this, so I am thinking return {apiCallable : useCallback(() => apiCall(state)) } but then I need to add [state] as a second param of useCallback. Then that means the callback will be re-rendered whenever state updates, so I'm back at the same issue, right?
This is my first time doing custom hooks like this. Having real difficulty understanding useCallback. How do I accomplish what I want?
Edit Put another way, I have lots of components that will dispatch small changes to deeply nested properties of this state, but this particular component must send the entire state object via a RESTful API, but otherwise will never use the state. It's irrelevant for rendering this component completely. I want to make it so this component never renders even when I'm making changes constantly to the state via keypresses on inputs (for example).
Since you provided Typescript types in your question, I will use them in my response.
Way One: Split Your Context
Given a context of the following type:
type ItemContext = {
items: Item[];
addItem: (item: Item) => void;
removeItem: (index: number) => void;
}
You could split the context into two separate contexts with the following types:
type ItemContext = Item[];
type ItemActionContext = {
addItem: (item: Item) => void;
removeItem: (index: number) => void;
}
The providing component would then handle the interaction between these two contexts:
const ItemContextProvider = () => {
const [items, setItems] = useState([]);
const actions = useMemo(() => {
return {
addItem: (item: Item) => {
setItems(currentItems => [...currentItems, item]);
},
removeItem: (index: number) => {
setItems(currentItems => currentItems.filter((item, i) => index === i));
}
};
}, [setItems]);
return (
<ItemActionContext.Provider value={actions}>
<ItemContext.Provider value={items}>
{children}
</ItemContext.Provider>
</ItemActionContext.Provider>
)
};
This would allow you to get access to two different contexts that are part of one larger combined context.
The base ItemContext would update as items are added and removed causing rerenders for anything that was consuming it.
The assoicated ItemActionContext would never update (setState functions do not change for their lifetime) and would never directly cause a rerender for a consuming component.
Way Two: Some Version of an Subscription Based Value
If you make the value of your context never change (mutate instead of replace, HAS THE WORLD GONE CRAZY?!) you can set up a simple object that holds the data you need access to and minimises rerenders, kind of like a poor mans Redux (maybe it's just time to use Redux?).
If you make a class similar to the following:
type Subscription<T> = (val: T) => void;
type Unsubscribe = () => void;
class SubscribableValue<T> {
private subscriptions: Subscription<T>[] = [];
private value: T;
constructor(val: T) {
this.value = val;
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.subscribe = this.subscribe.bind(this);
}
public get(): T {
return this._val;
}
public set(val: T) {
if (this.value !== val) {
this.value = val;
this.subscriptions.forEach(s => {
s(val)
});
}
}
public subscribe(subscription: Subscription<T>): Unsubscriber {
this.subscriptions.push(subscription);
return () => {
this.subscriptions = this.subscriptions.filter(s => s !== subscription);
};
}
}
A context of the following type could then be created:
type ItemContext = SubscribableValue<Item[]>;
The providing component would look something similar to:
const ItemContextProvider = () => {
const subscribableValue = useMemo(() => new SubscribableValue<Item[]>([]), []);
return (
<ItemContext.Provider value={subscribableValue}>
{children}
</ItemContext.Provider>
)
};
You could then use some a custom hooks to access the value as needed:
// Get access to actions to add or remove an item.
const useItemContextActions = () => {
const subscribableValue = useContext(ItemContext);
const addItem = (item: Item) => subscribableValue.set([...subscribableValue.get(), item]);
const removeItem = (index: number) => subscribableValue.set(subscribableValue.get().filter((item, i) => i === index));
return {
addItem,
removeItem
}
}
type Selector = (items: Item[]) => any;
// get access to data stored in the subscribable value.
// can provide a selector which will check if the value has change each "set"
// action before updating the state.
const useItemContextValue = (selector: Selector) => {
const subscribableValue = useContext(ItemContext);
const selectorRef = useRef(selector ?? (items: Item[]) => items)
const [value, setValue] = useState(selectorRef.current(subscribableValue.get()));
const useEffect(() => {
const unsubscribe = subscribableValue.subscribe(items => {
const newValue = selectorRef.current(items);
if (newValue !== value) {
setValue(newValue);
}
})
return () => {
unsubscribe();
};
}, [value, selectorRef, setValue]);
return value;
}
This would allow you to reduce rerenders using selector functions (like an extremely basic version of React Redux's useSelector) as the subscribable value (root object) would never change reference for its lifetime.
The downside of this is that you have to manage the subscriptions and always use the set function to update the held value to ensure that the subscriptions will be notified.
Conclusion:
There are probably a number of other ways that different people would attack this problem and you will have to find one that suits your exact issue.
There are third party libraries (like Redux) that could also help you with this if your context / state requirements have a larger scope.
Does this mean every change to FooContext will re-render the Foo component?
Currently (v17), there is no bailout for Context API. Check my another answer for examples. So yes, it will always rerender on context change.
It only needs to make use of state when someone clicks the button but never otherwise. Seems wasteful.
Can be fixed by splitting context providers, see the same answer above for explanation.
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?
What I want?
I want a mobx-react component to be binded on boxed observable primitive value from state. So I expect component to rerender if value changes.
Using lodash,
type BusinessData = { root: { path: { value: string } };
#observable state = buildInitialState();
const buildInitialState = () : BusinessData => {root: {}};
<BindedComponent value = {_.get(state, 'root.path.value')} />
const fetchState = () : BusinessData => { root: { path: { value: 'potato' } } };
What is the problem?
At the moment of first application rendering root.path is undefined. It will be fetched later on some stage of some internal component lifecycle or on user action. Furthermore, it might even not be fetched from server. Such path might not exist in data until user edits some input and this value will be set.
Supposable solution - initialize whole state explicitly:
const buildInitialState = () : BusinessData => { root: { path: { value: undefined } } };
Then BindedComponent can bind on boxed undefined and observe changes. This is bad, because when state is deep nested, I have to write such a boilerplate. And also in my case shape of business data can have a lof of implementations. So I have to initialize explicitly every one of them in all my projects.
Any ideas on how I can solve this without boilerplate?
Try to keep your state structure simple:
#observable state = { root: { path: { value: null } }
You can then create a simple update function:
async setStateValue(value) {
try {
this.state.root.path.value = await value
} catch (e) {
console.log(e)
}
}
Calling this at the rendering stage, will automatically update your component once the promise has been completed:
async updateFromComponent() {
await setStateValue('potato')
}
const {value} = prop.store.state.root.path
render (){
return (
<BindedComponent value = {value} />
)
}
I have a React Redux app which gets data from my server and displays that data.
I am displaying the data in my parent container with something like:
render(){
var dataList = this.props.data.map( (data)=> <CustomComponent key={data.id}> data.name </CustomComponent>)
return (
<div>
{dataList}
</div>
)
}
When I interact with my app, sometimes, I need to update a specific CustomComponent.
Since each CustomComponent has an id I send that to my server with some data about what the user chose. (ie it's a form)
The server responds with the updated object for that id.
And in my redux module, I iterate through my current data state and find the object whose id's
export function receiveNewData(id){
return (dispatch, getState) => {
const currentData = getState().data
for (var i=0; i < currentData.length; i++){
if (currentData[i] === id) {
const updatedDataObject = Object.assign({},currentData[i], {newParam:"blahBlah"})
allUpdatedData = [
...currentData.slice(0,i),
updatedDataObject,
...currentData.slice(i+1)
]
dispatch(updateData(allUpdatedData))
break
}
}
}
}
const updateData = createAction("UPDATE_DATA")
createAction comes from redux-actions which basically creates an object of {type, payload}. (It standardizes action creators)
Anyways, from this example you can see that each time I have a change I constantly iterate through my entire array to identify which object is changing.
This seems inefficient to me considering I already have the id of that object.
I'm wondering if there is a better way to handle this for React / Redux? Any suggestions?
Your action creator is doing too much. It's taking on work that belongs in the reducer. All your action creator need do is announce what to change, not how to change it. e.g.
export function updateData(id, data) {
return {
type: 'UPDATE_DATA',
id: id,
data: data
};
}
Now move all that logic into the reducer. e.g.
case 'UPDATE_DATA':
const index = state.items.findIndex((item) => item.id === action.id);
return Object.assign({}, state, {
items: [
...state.items.slice(0, index),
Object.assign({}, state.items[index], action.data),
...state.items.slice(index + 1)
]
});
If you're worried about the O(n) call of Array#findIndex, then consider re-indexing your data with normalizr (or something similar). However only do this if you're experiencing performance problems; it shouldn't be necessary with small data sets.
Why not using an object indexed by id? You'll then only have to access the property of your object using it.
const data = { 1: { id: 1, name: 'one' }, 2: { id: 2, name: 'two' } }
Then your render will look like this:
render () {
return (
<div>
{Object.keys(this.props.data).forEach(key => {
const data = this.props.data[key]
return <CustomComponent key={data.id}>{data.name}</CustomComponent>
})}
</div>
)
}
And your receive data action, I updated a bit:
export function receiveNewData (id) {
return (dispatch, getState) => {
const currentData = getState().data
dispatch(updateData({
...currentData,
[id]: {
...currentData[id],
{ newParam: 'blahBlah' }
}
}))
}
}
Though I agree with David that a lot of the action logic should be moved to your reducer handler.