I'm currently writing a bus timetable application that uses object types to model timetable "documents".
interface Timetable { name: string stops: string[] services: string[][] }
In addition to types, I have many functions, and if I'm going to use mutations, I usually write them as methods on the class. I mainly use Immer so I don't have to write a lot of extended syntax. For example,
const addStop = (timetable: Timetable, stopName: string): Timetable => { return produce(timetable, (newTimetable) => { newTimetable.stops.push(stopName) }) }
To manage state I use Zustand and Immer, but I feel like if I used Redux my problem would be the same. In my store I have an array of Timetable objects, and an operation that also uses Immer to reassign the currently selected Timetable object:
updateTt: (tt, index) => { set((state) => { state.timetables[index] = tt }) }, updateThisTt: (timetable) => { set((s) => { if (s.selectedTtIdx === null) { throw new Error("no selected timetable") } s.timetables[s.selectedTtIdx] = timetable }) },
Then I call the data change function in the React component and call the update operation:
const onAddStop = (name) => { updateThisTt(addStop(timetable, name)) }
This works, but I'm not sure if I'm doing it correctly. I now have two layers of Immer calls, my component now has data modifying functions that are called directly in its event handlers, and I don't really like the look of the "methods" even though overall it's a minor bug.
I have considered:
Timetable
to a class, rewrite the data modification functions as mutation methods, and set [immerable] = true
and let Immer do all the work for me action. I've done this, but I'd rather stick to the immutable record mode. For what it's worth, the documentation for Flux, Zustand, or Immer tends to show the first option, and only occasionally; no application is as simple as counter = counter 1
. What is the best way to build an application using the Flux architecture?
(I'm not familiar with Zusand and Immer, but maybe I can help...)
There are always different ways and I'm suggesting my favorite one here.
Clearly distinguish between "scheduling" actions and actual "mutations" of state. (Maybe add another level in between).
Specific "mutation" function
I recommend creating specific "mutation" functions rather than generic ones, i.e.:
updateThisTt: () => { ...
,addStop: () => { ...
.Create as many mutation functions as needed, each with a purpose.
Constructing new state in "mutation" function
Conceptually, use
immer
generators only within mutation functions.(I mean about the store. Of course, you can still use
immer
for other purposes) .According to this official example:
Inside the component you can now call the "mutator" function.
Building a new country
If building the new state gets complicated, you can still extract some "builder" functions. But first consider the next section, "Large Files and Duplication."
For example your
addStop
function can also be called inside a Zustand mutation:Large Files and Duplication
Code duplication should certainly be avoided, but there are always trade-offs.
I won't suggest a specific approach, but please note that code is usually read more than written . Sometimes I think it's worth writing a few more letters, for example something like
state.timetables[index]
multiple times, If it makes the purpose of the code more obvious. You need to judge for yourself.Anyway, I recommend putting your mutation function into a separate file that doesn't do anything else, This way, it may seem easier to understand than you think.
If you have a very large file but are completely focused only on modifying the state , And the structure is consistent, making it easy to read even if you have to scroll A few pages.
For example if it looks like this:
Also note that these variadic functions are completely independent (or are they? I expect Zustand to use pure functions here, no?) .
This means that if it gets complicated, you can even split up multiple mutation functions Split into separate files within a folder Like
/store/mutators/timeTable.js
.But you can easily do this anytime later.
Building the "payload" ("action caller")
You may feel you need another "level" Between Event handler and Mutation function. I usually have a layer like this, but I don't have a good name for it. We will temporarily refer to this as "Operation Caller".
Sometimes it is difficult to decide what belongs to the "variation function" and what belongs to the "variation function" "Action caller".
Anyway, you can "build some data" inside the "action caller", but you should No state operations of any kind are performed here, not even using
immer
.Transforming state and creating payload
This is a subtle distinction and you probably shouldn't worry too much about it (and there may be exceptions) , but as an example:
You can use parts of the old state within an "action caller", for example:
But you should not notmap (or convert) the old state to the new state, for example:
another example
No specific suggestions are made here, just an example:
It can get messy passing many values to mutators, for example:
In the event handler you want to focus on what you really want to do, like "add new item", But don’t consider all arguments. Additionally, you may need the new item for other things.
Or you can write: