React和Redux架构问题

时间:2017-11-24 08:50:39

标签: javascript reactjs redux

阅读前:

这不是非工作代码的问题,而是关于架构的问题。此外,我目前还没有使用ReactRedux库,我首先尝试了解这个部分在这个测试应用程序中如何工作。它尽可能短,但不幸的是仍然很长,请耐心等待我

简介

我有一系列Bottle型号。使用伪代码,瓶子的定义如下:

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
}

我还有一个显示所有Bottle项目的React组件。组件也需要存储所有Bottle项目的先前状态(用于设置动画,忽略它)。

Redux使用

我需要使用辅助类来对某些Bottle项执行复杂的操作,如下所示:

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

我不想为每个操作更新商店,因为我希望商店最终一次更新。因此我的BottleReducer看起来像这样:

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

其中action.updated_bottles是执行操作后瓶子的最终状态。

问题

即使一切正常,我仍然怀疑这是接近我的架构的"错误的心态" 。其中一个原因是为了避免保留对瓶子对象的引用并在执行操作时改变状态,我必须做这个丑陋的事情:

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

这是因为我需要一个仍然保留其原始功能的克隆对象数组(意味着它们是该类的实际实例克隆)

如果你能指出我逻辑中的缺陷/缺陷,我将不胜感激。

P.S:我需要保持"深度克隆的原因"类实例是这样的,我可以在我的React组件中保持瓶子的先前状态,以便在渲染更新发生时在两种状态之间进行动画制作。

3 个答案:

答案 0 :(得分:4)

在处理redux架构时,将序列化和不变性保持在每个决策的最前沿是非常有用的,一开始这可能很困难,尤其是当你习惯于OOP时

由于商店的状态只是一个JS对象,因此很容易使用它来跟踪更复杂的模型类的JS实例,而应该更像是一个数据库,在那里你可以序列化模型的表示以不可改变的方式来往于此。

以最原始的形式存储瓶子的数据表示,使得对于localStorage的持久性和商店的再水化成为可能,因为更高级的应用程序可以允许服务器端渲染并可能离线使用,但更重要的是它使得它更多更可预测,更明显的是您的应用程序中发生的变化和变化。

我见过的大多数redux应用程序(包括我的)沿着功能路线完全取消了模型类,只是简单地在reducers中执行操作 - 可能会使用帮助程序。这样做的一个缺点是它使得大型复杂的减速器缺乏一些背景。

然而,如果您希望将这些帮助程序封装到Bottle类中,那么有一个中间立场是完全合理的,但您需要考虑案例类,它可以是创建并序列化回数据表单如果在 上进行操作,则行为不可动摇

让我们看看这对你的瓶子有什么用处(打字注释用来帮助显示发生的事情)

瓶子类

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)
  }

}

存储状态 请注意,它包含数据表示的数组,而不是类实例

interface IBottleStore {
  bottles: Array<IBottle>
}

瓶子选择器 在这里,我们使用选择器从存储中提取数据并执行转换为类实例,您可以将其作为prop传递给React组件。 如果使用像reselect这样的lib,那么这个结果将被记忆,因此在商店中的基础数据发生变化之前,您的实例引用将保持不变。 这对于使用PureComponent优化React很重要,PureComponent只通过引用来比较props。

const bottlesSelector = (state: IBottleStore): Array<Bottle> => state.bottles.map(v => Bottle.fromJSON(v))

瓶子减速机 在您的Reducer中,您可以使用Bottle类作为帮助程序来执行操作,而不是直接在reducer中对数据本身执行所有操作

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
  }
}

虽然这个drink / fill示例相当简单,并且可以直接在reducer中的数据上尽可能轻松地完成,但它说明了使用case类来表示数据在更真实的世界中,仍然可以完成术语,并且可以比在视图中使用巨型缩减器和手动计算属性更容易理解和保持代码更有条理,并且作为奖励,Bottle类也很容易测试。

通过在整个过程中不可动作,如果设计正确,你的React类的先前状态将继续保持对你之前的瓶子的参考(在他们自己的先前状态),所以没有必要以某种方式跟踪你自己做动画等

答案 1 :(得分:1)

如果Bottle类是反应组件(或反应组件内),我认为你可以使用 componentWillUpdate(nextProps,nextState ),这样你就可以检查以前的状态(不要卸载你的组件)课程)。 https://reactjs.org/docs/react-component.html#componentwillupdate

深入克隆你的课程对我来说似乎不是一个好主意。

修改 “我还有一个显示所有Bottle项目的React组件。” 那是你应该保留并寻找你以前的状态的地方。将所有瓶子放在瓶子里。当你需要展示瓶子时,将它放入你的组件中。

在componentWillUpdate中,您可以检查this.state(这是您更新之前的状态,即您以前的状态)和 nextState作为参数传递这是当前状态

<强> EDIT2:

为什么你要在你的州保持完整的课程? 只需将数据保持在状态。我的意思是只保留一个将由reducer更新的对象。如果你需要一些utils函数(解析器......)不要让它们处于你的状态,在更新你的状态之前在reducers中处理你的数据或者在一些utils文件中保存你的utils / parser函数 < / p>

你的州也应该保持不变。因此,这意味着您的reducer应该返回更新状态的副本。

答案 2 :(得分:0)

  

我有一系列的瓶型号。

我认为拥有一个BottleCollection模型更有意义。

或者你可能有一个瓶型号和它的多种用途?

class Bottle{
    //members
    filledLiters
    filledLitersCapacity
    otherMember1
    otherMember2

    //functions
    toPostableObject(){}
}

嗯,看起来你的模型代表了很多东西:

  • 持久数据的缓存(通过AJAX检索?)
  • 数据对象(哑字段)
  • 用户输入的临时状态(要发布的数据?)

我不会称之为模特。它包含3件事:API包装器/缓存,数据和待定更改。

我称之为REST API包装器,数据对象和应用程序状态。

  

我需要使用辅助类来对某些Bottle项执行复杂的操作,如下所示:

var updated_bottles = 
    BottleHandler.performOperationsOnBottles(bottleIds)

它看起来是域逻辑。我不会将应用程序的核心逻辑放在&#34; helper class&#34;的名称下。我会把它称为&#34;模型&#34;或&#34;业务规则&#34;。

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

这看起来是应用程序状态的变化。但我不明白它的原因。即谁要求这种改变以及为什么?

  

我不想为每个操作更新商店,因为我希望商店能够一次性更新商店。

这是一个很好的推理。

因此,您将拥有单一操作类型:

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

但是,在这种情况下,您可能需要像这样清理旧状态:

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

我需要保持&#34;深度克隆的原因&#34;类实例是为了让我可以保持以前的瓶子状态

我认为原因是您在单个对象中保留REST API缓存和挂起的更改。如果您在单独的对象中保留缓存和挂起的更改,则不需要克隆。

需要注意的另一件事是你的状态应该是一个普通的JavaScript对象,而不是一个类的实例。如果您知道状态包含哪种类型的数据,那么没有理由在状态中保留对函数(实例方法)的引用。您可以只使用临时类实例:

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