43688

React and Redux architecture issues

Question:

<strong>Before reading:</strong>

<em>This isnt a matter of non working code but a question on architecture. Also i am not currently using the ReactRedux library as im first trying to understand how the parts work on their own in this test app. Its as short as i could cut it but unfortunately still lengthy, please bear with me</em>

<strong>Short Intro</strong>

I've got an array of Bottle models. Using pseudocode,a bottle is defined like so:

class Bottle{ //members filledLiters filledLitersCapacity otherMember1 otherMember2 //functions toPostableObject(){ //removes functions by running JSON. var cloneObj = JSON.parse(JSON.stringify(this)); //removes all members we dont want to post delete cloneObj["otherMember1"]; } //other functions }

I've also got a React component that displays all Bottle items.The component needs to store the previous state of all Bottle items as well ( its for animating, disregard this ).

<strong>Redux usage</strong>

There are complex operations i need to perform on some of the Bottle items using a helper class like so:

var updated_bottles = BottleHandler.performOperationsOnBottles(bottleIds) mainStore.dispatch({type:"UPDATED_BOTTLES",updated_bottles:updated_bottles})

I dont want to update the store for every operation as i would like the store to be updated all together at the end in one go. Therefore my BottleReducer looks something like this :

var nextState = Object.assign({}, currentState); nextState.bottles = action.updated_bottles

Where action.updated_bottles is the final state of bottles after having performed the operations.

<strong>The issue</strong>

Even though everything works, im suspicious that this is the <strong>"wrong mindset"</strong> for approaching my architecture. One of the reasons is that to avoid keeping the reference to the bottle objects and mutating the state as im performing the operations, i have to do this ugly thing:

var bottlesCloneArray = mainStore.getState(). bottleReducer.bottles.map( a => { var l = Object.assign({}, a); Object.setPrototypeOf( l, Character.prototype ); return l } );

This is because i need a cloned array of objects that still retain their original functions ( meaning they're actual instance clones of the class )

If you can point out the flaw/flaws in my logic i'd be grateful.

<em>P.S: The reason i need to keep "deep clones" of the class instances is so that i can keep the previous state of bottles in my React component for the reason of animating between the two states when an update in render happens.</em>

Answer1:

When dealing with redux architecture it can be extremely useful to keep serialisation and immutability at the forefront of every decision, this can be difficult at first especially when you are very used to OOP

As the store's state is just a JS object it can be tempting to use it to keep track of JS instances of more complex model classes, but instead should be treated more like a DB, where you can serialise a representation of your model to and from it in an immutable manner.

Storing the data representations of your bottles in its most primitive form makes things like persistance to localStorage and rehydration of the store possible for more advanced applications that can then allow server side rendering and maybe offline use, but more importantly it makes it much more predictable and obvious what is happening and changing in your application.

Most redux apps i've seen (mine included) go down the functional route of doing away with model classes altogether and simply performing operations in the reducers directly upon the data - potentially using helpers along the way. A downside to this is that it makes for large complex reducers that lack some context.

However there is a middle ground that is perfectly reasonable if you prefer to have such helpers encapsulated into a Bottle class, but you need to think in terms of a <strong>case class</strong>, which can be <em>created from and serialised back to the data form</em>, and <em>acts immutably if operated upon</em>

Lets look at how this might work for your Bottle (typescript annotated to help show whats happening)

<strong>Bottle case class</strong>

<pre class="lang-js prettyprint-override">interface IBottle { name: string, filledLitres: number capacity: number } class Bottle implements IBottle { // deserialisable static fromJSON(json: IBottle): Bottle { return new Bottle(json.name, json.filledLitres, json.capacity) } constructor(public readonly name: string, public readonly filledLitres: number, public readonly capacity: number) {} // can still encapuslate computed properties so that is not needed to be done done manually in the views get nameAndSize() { return `${this.name}: ${this.capacity} Litres` } // note that operations are immutable, they return a new instance with the new state fill(litres: number): Bottle { return new Bottle(this.name, Math.min(this.filledLitres + litres, this.capacity), this.capacity) } drink(litres: number): Bottle { return new Bottle(this.name, Math.max(this.filledLitres - litres, 0), this.capacity) } // serialisable toJSON(): IBottle { return { name: this.name, filledLitres: this.filledLitres, capacity: this.capacity } } // instances can be considered equal if properties are the same, as all are immutable equals(bottle: Bottle): boolean { return bottle.name === this.name && bottle.filledLitres === this.filledLitres && bottle.capacity === this.capacity } // cloning is easy as it is immutable copy(): Bottle { return new Bottle(this.name, this.filledLitres, this.capacity) } }

<strong>Store state</strong> Notice it contains an array of the <em>data representation</em> rather than the class instance

<pre class="lang-js prettyprint-override">interface IBottleStore { bottles: Array<IBottle> }

<strong>Bottles selector</strong> Here we use a selector to extract data from the store and perform transformation into class instances that you can pass to your React component as a prop. If using a lib like reselect this result will be memoized, so your instance references will remain the same until their underlying data in the store has changed. This is important for optimising React using PureComponent, which only compares props by reference.

<pre class="lang-js prettyprint-override">const bottlesSelector = (state: IBottleStore): Array<Bottle> => state.bottles.map(v => Bottle.fromJSON(v))

<strong>Bottles reducer</strong> In your reducers you can use the Bottle class as a helper to perform operations, rather than doing everything right here in the reducer directly on the data itself

<pre class="lang-js prettyprint-override">interface IDrinkAction { type: 'drink' name: string litres: number } const bottlesReducer = (state: Array<IBottle>, action: IDrinkAction): Array<IBottle> => { switch(action.type) { case 'drink': // immutably create an array of class instances from current state return state.map(v => Bottle.fromJSON(v)) // find the correct bottle and drink from it (drink returns a new instance of Bottle so is immutable) .map((b: Bottle): Bottle => b.name === action.name ? b.drink(action.litres) : b) // serialise back to date form to put back in the store .map((b: Bottle): IBottle => b.toJSON()) default: return state } }

While this drink/fill example is fairly simplistic, and could be just as easily done in as many lines directly on the data in the reducer, it illustrate's that using case class's to represent the data in more real world terms can still be done, and can make it easier to understand and keep code more organised than having a giant reducer and manually computing properties in views, and as a bonus the Bottle class is also easily testable.

By acting immutably throughout, if designed correctly your React class's previous state will continue to hold a reference to your previous bottles (in their own previous state), so there is no need to somehow track that yourself for doing animations etc

Answer2:

If Bottle class is a react component (or inside a react component) I think you could play with <strong>componentWillUpdate(nextProps, nextState</strong>) so you can check the previous state (do not unmount your component of course). <a href="https://reactjs.org/docs/react-component.html#componentwillupdate" rel="nofollow">https://reactjs.org/docs/react-component.html#componentwillupdate</a>

Deep cloning your class doesn't seem a good idea to me.

<strong>Edit:</strong> "I've also got a React component that displays all Bottle items." That's where you should keep and look for your previous state. Keep all your bottle in a bottles store. And get it in your components when you need to display bottles.

Inside componentWillUpdate you can check you this.state (which is your state just before being updated, ie your previous state) and <strong>nextState passed as a parameter</strong> which is the current state

<strong>Edit2:</strong>

why would you keep an complete class in your state ? <strong>Just keep data in state</strong>. I mean just keep <strong>an object that will be updated by your reducer</strong>. If you need to have some utils functions (parser...) do not keep them in your state, <strong>treat your data in reducers before updating your state or keep your utils/parser functions in some utils file</strong>

Also your state should stay immutable. So it means you reducer should return a copy of the updated state anyway.

Answer3:

<blockquote>

I've got an array of Bottle models.

</blockquote>

I think It makes more sense to have a model of BottleCollection.

Or maybe you have one Bottle model and multiple usages of it?

class Bottle{ //members filledLiters filledLitersCapacity otherMember1 otherMember2 //functions toPostableObject(){} }

Hm, it looks like your model represents multiple things:

<ul><li>a cache of persistent data (retrieved via AJAX?)</li> <li>data object (dumb fields)</li> <li>a temporary state for user input (data to be POSTed?)</li> </ul>

I wouldn't call it a model. It's 3 things: API wrapper/cache, data and pending changes.

I would call it REST API wrapper, data object and application state.

<blockquote>

There are complex operations i need to perform on some of the Bottle items using a helper class like so:

var updated_bottles = BottleHandler.performOperationsOnBottles(bottleIds) </blockquote>

It looks to be the domain logic. I wouldn't place the core logic of the application under the name "helper class". I would call it "the model" or "business rules".

<blockquote> mainStore.dispatch({type:"UPDATED_BOTTLES", updated_bottles:updated_bottles}) </blockquote>

That looks to be a change in application state. But I don't see the reason for it. I.e. who requested this change and why?

<blockquote>

I dont want to update the store for every operation as i would like the store to be updated all together at the end in one go.

</blockquote>

That's a good reasoning.

So you'll have a single action type:

mainStore.dispatch({type:"UPDATED_DATA", { updated_bottles })

However, in this case you might need to clean up old state like this:

mainStore.dispatch({type:"UPDATED_DATA", { updated_bottles: null }) <blockquote>

The reason i need to keep "deep clones" of the class instances is so that i can keep the previous state of bottles

</blockquote>

I think the reason is that you keep REST API cache and pending changes in a single object. If you keep cache and pending changes in separate objects you don't need clones.

Another thing to note is that your state should be a plain JavaScript object, not an instance of a class. There's no reason to keep references to functions (instance methods) in a state if you know which type of data your state contains. You can just use temporary class instances:

const newBottlesState = new BottleCollection(state.bottlesCache, state.bottlesUserChanges).performOperationsOnBottles()

Recommend

  • Why don't tuples get the same ID when assigned the same values?
  • Eliminate Duplicate C# Property Bodies
  • Python are strings immutable [duplicate]
  • using IFileOperation in delphi 7
  • How to implement “Registry pattern” in C++, using single registry for many interface implementation
  • VSTO with Windows Form and Worker Threads
  • YAML reusable variables with case-specific values
  • Is func_globals mutable?
  • Scanning the log files for last 30 minutes of data
  • Multiple arguments for a PHP function
  • Serialising my class is failing because of an eventhandler
  • REACT: Add highlighted border around selected image [closed]
  • creating a UI in background thread WPF?
  • How to access COM objects from different apartment models?
  • Boost Graph as basis for a simple DAG Graph?
  • Cannot run nunit tests with Nant
  • WPF and background worker and the calling thread must be STA
  • 407 Proxy Authentication Required
  • Custom Json (de)serialisation?
  • Is it possible to insert three numbers into 2 bytes variable?
  • Is it possible to instantiate an object of one class in two different ways?
  • Docker container doesn't start, showing as 'Exited n seconds ago'
  • Regarding RandomTree in Weka
  • “babelHelpers.asyncToGenerator is not a function” on React-Native 0.16.0 and 0.17.0
  • C# OpenFileDialog Thread start but dialog not shown
  • hexagonal lattice which is randomly colored with black and white color
  • Is it a bad practice to rely on local objects get destructed in the reverse order of construction in
  • Feature Event Handler called multiple times for Farm level feature - sharepoint 2007
  • How To Pass Props From Redux Store into Apollo GraphQL Query
  • Java EE 6 Login module
  • How to make VBA count from 0 and not 1 in Excel Macro
  • JAR doesn't work with Absolute Layout
  • ZeroMQ poll thread safety
  • initializing array of variable size inside a class
  • React Native + Redux: What's the best and preferred navigation?
  • How to lookup value with multiple criteria in excel 2007 and newer
  • Neo4j one-to-many fetch data
  • Invert string in Rust
  • Less Conflicting Session Manager for Zope 2
  • calculate gradient output for Theta update rule