concent 骚操做之组件建立&状态更新

❤ star me if you like concent ^_^css

进化中的组件

随着react 16.8发布了稳定版本的hook特性,原来官网文档里对SFC的描述也修改成了FC,即无状态函数组件变动为了函数组件,官方代言人Dan Abramov也在各类场合开始向社区力推hook,将其解读为下一个5年React与时俱进的开端。html

仔细想一想,其实hook只是改变了咱们组织代码的方式,由于hook的存在,咱们原来在类组件里的各类套路均可以在函数组件里找到一一对应的写法,可是依托于class组件创建起来一系列最佳实践在hook组件里所有都要改写,因此官方也是推荐如非必要,为了稳妥起见老项目里依然使用class组件react

任何新技术的出现必定都是有相关利益在驱动的,hook也不例外,官网对hook出现的动机给了3点重要解释git

  • 在组件之间复用状态逻辑很难
  • 复杂组件变得难以理解
  • 难以理解的 class

固然class组件最为诟病的包裹地狱由于hook独特的实现方式被消除了,因此class组件彷佛在将来的日子里将慢慢被冷落掉,而hook本质只是一个个函数,对函数式编程将变得更加友好,同时还能继续推动组合大于继承的中心思想,让更多的开发者受益于这种全新的开始思路并提高开发体验。github

按照官方的愿意表达,Hook既拥抱了函数,同时也没有牺牲 React 的精神原则,提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。typescript

concent如何看待组件

前面有一句话提到「任何新技术的出现必定都是有相关利益在驱动的」,因此concent的诞生的动机也是很是明确:编程

  • 让类组件和函数组件拥有彻底一致的编码思路和使用体验
  • 用最少的代码表达状态共享、逻辑复用等问题
  • 从组件层面搭建一个更优的最小化更新机制
  • 加强组件,赋予组件更多的强大特性

上面提到的第一点其实说白了统一类组件和函数组件,得益于concent能为组件注入实例上下文的运行机制,不管是从api使用层面仍是渲染结果层面,都将高度给你一致的体验,因此在concent眼里,类与函数都是ui表达的载体而已,再也不区分对待它们,给用户更多的选择余地。api

那么废话少说,咱们直接开整,看看concent提供了多少种建立组件很更新状态的方式。数组

在展现和解读组件建立和状态更新代码以前,咱们先使用run接口载入一个示例的业务model名为demo,在如下代码结构处于models文件夹。bash

这里一个示例项目文件组织结构,不一样的人可能有不一样的理解方式和组织习惯,这里只是以一个基本上社区上公认的通用结构做为范原本为后面的代码解读作基础,实际的文件组件方式用户能够根据本身的状况作调节

|____runConcent.js      # concent启动脚本
|____App.css
|____index.js           # 项目的入口文件
|____models             # 业务models
| |____index.js
| |____demo             # [[demo模块定义]]
| | |____reducer.js     # 更新状态(可选)
| | |____index.js       # 负责导出demo model
| | |____computed.js    # 定义计算函数(可选)
| | |____init.js        # 定义异步的状态初始化函数(可选)
| | |____state.js       # 定义初始状态(必需)
| |____...
| 
|____components         # [[基础组件]]
| |____layout           # 布局组件
| |____bizsmart         # 业务逻辑组件(能够含有本身的model)
| |____bizdumb          # 业务展现组件
| |____smart            # 平台逻辑组件(能够含有本身的model)
| |____pure             # 平台展现组件
| 
|____assets             # 会被一块儿打包的资源文件
|____pages              # 路由对应的页面组件(能够含有本身的model,即page model)
| |____...
| |
|____App.js
|____base
| |____config           # 配置
| |____constant         # 常量
|____services           # 业务相关服务
|____utils              # 通用工具函数

复制代码

demo的state定义

export function getInitialState(){
    return {
        name: 'hello, concent',
        age: 19,
        visible: true,
        infos: [],
    }
}

export default getInitialState();
复制代码

使用run接口载入模块定义

// code in runConcent.js
import models from 'models';
import { run } from 'concent';

run(models);
复制代码

对以上实例代码有疑问能够参考往期文章:
聊一聊状态管理&Concent设计理念
使用concent,体验一把渐进式地重构react应用之旅
或者直接查看官网文档了解更多细节

建立类组件

使用register接口直接将一个普通类组件注册为concent类组件

import { register } from 'concent';
import React, { Component } from 'react';

@register('demo')
export default class ClassComp extends Component {
  render() {
    const { name, age, visible, infos } = this.state;
    return <div>...your ui</div>
  }
}
复制代码

是的你没看错,这就完成了concent类组件的注册,它属于demo模块,state里将自动注入demo模块的全部数据,让咱们把它渲染出来,看看结果

function App() {
  return (
    <div>
      <ClassComp />
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));
复制代码

打开ReactDevTool查看dom结构

能够看到顶层没有任何Provider,数据直接打入组件内部,同时组件自己没有任何包裹,只有一层,由于默认采用反向继承的hoc策略,你的渲染的组件再也不产生大量Wrapper Hell...

或许有小伙伴会问这样会不会打破了hoc模式的约定,由于你们都是使用属性代理方式来作组件修饰,不破坏组件原有的任何结构,同时还能复用逻辑,但是这里咱们须要多思考一下,若是逻辑复用不必定非要从属性上穿透下来,而是直接能从实例上下文里提供,那为什么咱们非要墨守成规的使用属性代理的hoc模式呢?

固然concent对于类的修饰虽然默认使用了反向继承,可是也容许用户使用属性代理,只须要开启一个标记便可

@register({ module: 'demo', isPropsProxy: true })
export default class ClassComp extends Component{
  constructor(props, context){
    super(props, context);
    this.props.$$attach(this);// 属性代理模式须要补上这句话,方便包裹层接管组件this
  }
  render(){
    const {name, age, visible, infos} = this.state;
    return <div>...your ui</div>
  }
}
复制代码

显而易见的,咱们发现已经多了一层包裹,之因此提供 isPropsProxy参数,是由于有些组件用到了多重装饰器的用法,因此为了避免破坏多重装饰器下的使用方式而提供,但大多数时候,你都应该忘记这种用法,让react dom树保持干净清爽何乐而不为呢?

图中咱们看到组件名时$$CcClass1,这是一个当用户没有显示指定组件名时,concent本身起的名字,大多数时候咱们能够给一个与目标包裹组件同名的名字做为concent组件的名字

//第二个可选参数是concent组件名
@register('demo', 'ClassComp')
export default class ClassComp extends Component{...}
复制代码

建立CcFragment组件

CcFragment是concent提供的内置组件,可让你不用定义和注册组件,而是直接在视图里声明一个组件实例来完成快速消费某个模块数据的实例。

咱们在刚才的App里直接声明一个视图消费demo模块的数据

function App() {
  return (
    <div> <ClassComp /> <CcFragment register="demo" render={ctx => { const { name, age, visible, infos } = ctx.state; return <div>...your ui</div> }} /> </div> ); } 复制代码

渲染结果以下图所示:

CcFragment采用的是 Render Props方式来书写组件,特别适合一些临时多个模块数据的视图片断

<CcFragment register={{connect:['bar', 'baz']}} render={ctx => {
        // 该片断链接了bar,baz两个模块,消费它们的数据
        const { bar, baz } = ctx.connectedState;
        return <div>...your ui</div>
      }} />
复制代码

基于registerDumb建立组件

用户一般在某些场合会基于CcFragment作经一步的封装来知足一些高纬抽象的需求,concent自己也提供了一个接口registerDumb来建立组件,它本质上是CcFragment的浅封装

const MyFragment = registerDumb('demo', 'MyFragment')(ctx=>{
  const { name, age, visible, infos } = ctx.state;
  return  <div>...I am MyFragment</div>
})
复制代码

渲染结果以下图所示:

能够看到react dom tree上,出现了3层结构,最里面层是无状态组件实例。

基于hook建立组件

虽然registerDumb写起来像函数组件了,但实际上出现了3层结构不是咱们但愿看到的,咱们来使用hook方式重构此组件吧,concent提供了useConcent接口来建立组件,抹平类组件与函数组件之间的差别性。

function HookComp(){
  const ctx = useConcent('demo', 'HookComp');
  const { name, age, visible, infos } = ctx.state;
  return  <div>...I am HookComp</div>
}
复制代码

渲染结果以下图所示:

基于registerHookComp建立组件

registerHookComp本质上是useConcent的浅封装,自动帮你使用React.memo包裹

const MemoHookComp = registerHookComp({
  module:'demo',
  render: ctx=>{
    const { name, age, visible, infos } = ctx.state;
    return  <div>...I am MemoHookComp</div>
  }
});
复制代码

渲染结果图里咱们能够看到tag上有一个Memo,那是React.memo包裹组件后DevTool的显示结果。

concent如何看待状态更新

上面的全部组件示例里,咱们都只是完成的模块状态的获取和展现,并无作任何更新操做,接下来咱们将对组件加入状态更新操做行为。

利用setState完成状态更新

由于concent已接管了setState行为,因此对于使用者来讲,setState就能够完成你想要的状态更新与状态同步。

在替换setState前,concent会保持一份引用reactSetState指向原始的setState,因此你大可没必要担忧setState会影响react的各类新特性诸如fiber 调度time slicing异步渲染等,由于concent只是利用接管setState后完成本身的状态分发调度工做,自己是不会去破坏或者影响react自身的调度机制。

// 改写ClassComp
@register('demo')
export default class ClassComp extends Component {
  changeName = (e)=> this.setState({name:e.currentTarget.value})
  render() {
    const { name } = this.state;
    return <input value={name} onChange={this.changeName} /> } } 复制代码
// 改写ClassComp
  <CcFragment register="demo" render={ctx => {
    const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
    const { name, age, visible, infos } = ctx.state;
    return <input value={name} onChange={changeName} /> }} /> 复制代码
// 改写MyFragment
registerDumb('demo', 'MyFragment')(ctx=>{
  const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
  const { name, age, visible, infos } = ctx.state;
  return <input value={name} onChange={changeName} /> }) 复制代码
// 改写HookComp
function HookComp(){
  const ctx = useConcent('demo', 'HookComp');
  const { name, age, visible, infos } = ctx.state;
  const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
  return <input value={name} onChange={changeName} /> } 复制代码
// 改写MemoHookComp
const MemoHookComp = registerHookComp({
  module:'demo',
  render: ctx=>{
    const { name, age, visible, infos } = ctx.state;
    const changeName = (e)=> ctx.setState({name:e.currentTarget.value});
    return  <input value={name} onChange={changeName} /> } }); 复制代码

能够看到,因此的组件都是同样的写法,不一样的是类组件还存在着一个this关键字,而在函数组件里都交给ctx去操做了。

如今让咱们经过gif图演示看看实际效果吧

由于这些实例都是属于demo模块的组件,因此不管我修改任何一处,其余地方视图都会同步被更新,是否是将特别方便呢?

使用sync更新

固然若是对于这种单个key的更新,咱们也能够不用写setState,而是直接使用concent提供的工具函数sync来完成值的提取与更新

// 改写HookComp使用sync来更新,其余组件写法都同样,class组件经过this.ctx.sync来更新
function HookComp(){
  const ctx = useConcent('demo', 'HookComp');
  const {state: { name, age, visible, infos }, sync } = ctx.state;
  return <input value={name} onChange={sync('name')} /> } 复制代码

使用dispatch更新

当咱们的业务逻辑复杂的时候,在真正更新以前要作不少数据的处理工做,这时咱们能够将其抽到reducer

// 定义reducer,code in models/demo/reducer.js

export updateName(name, moduleState, actionCtx){
  return {name, loading: false};    
}

export updateNameComplex(name, moduleState, actionCtx){
   // concent会自动在reducer文件内生成一个名为setState的reducer函数,免去用户声明一次
   await actionCtx.setState({loading:true});
   await api.updateName(name);
   // 在同一个reducer文件类的函数,能够直接基于函数引用调用
   await actionCtx.dispatch(updateName, name);
}
复制代码

在组件内部使用dispatch触发更新

function HookComp(){
  const ctx = useConcent('demo', 'HookComp');
  const { name, age, visible, infos } = ctx.state;
  const updateNameComplex = (e)=>ctx.dispatch('updateNameComplex', e.currentTarget.value);
  return <input value={name} onChange={updateNameComplex} /> } 复制代码

固然,这里有更优的写法,使用setup静态的定义相关接口。了解更多关于setup

const setup = ctx=>{
  //这里其实还能够搞更多的事儿,诸如ctx.computed, ctx.watch, ctx.effect 等,下期再聊✧(≖ ◡ ≖✿)
  return {
    updateNameComplex: (e)=>ctx.dispatch('updateNameComplex',e.currentTarget.value),
  }
}
function HookComp(){
  // setup只会在组件初次渲染以前触发一次!
  const ctx = useConcent({module:'demo', setup}, 'HookComp');
  const { name, age, visible, infos } = ctx.state;
  return <input value={name} onChange={ctx.settings.updateNameComplex} /> } 复制代码

使用invoke更新

invoke给予用户更自由的灵活程度来更新视图数据,由于本质来讲concent的reducer函数就是一个个片断状态生成函数,因此invoke让用户能够不须要走dispatch套路来更新数据。

由于reducer定义是跟着model走的,为了规范起见,实际编码过程当中定义reducer函数比invoke更可以统一数据更新流程,很方便查看和排除bug。

function updateName(name, moduleState, actionCtx){
  return {name, loading: false};    
}

function updateNameComplex(name, moduleState, actionCtx){
   await actionCtx.setState({loading:true});
   await api.updateName(name);
   await actionCtx.invoke(updateName, name);
}

const setup = ctx=>{
  return {
    updateNameComplex: (e)=>ctx.invoke(updateNameComplex,e.currentTarget.value),
  }
}
function HookComp(){
  const ctx = useConcent({module:'demo', setup}, 'HookComp');
  const { name, age, visible, infos } = ctx.state;
  return <input value={name} onChange={ctx.settings.updateNameComplex} /> } 复制代码

结语

经过以上示例,读者应该能体会到统一类组件和函数组件的好处,那就是知足你任什么时候段渐进式的书写你的应用,不管是组件的定义方式和数据的修改方式,你均可以按需采起不一样的策略,并且concent里的hook使用方式是遵循着reducer承载核心业务逻辑,dispatch派发修改状态的经典组织代码方式的,可是并无强制约束你必定要怎么写,给予了你最大的自由度和灵活度,沉淀你我的的最佳实践,甚至你能够经过修改少许的代码来100%复制社区里现有的公认最佳实践到你的concent应用。

(下2期预告:1 探究setup带来的变革;2 concent love typescript,指望读者多多支持,concent,冲鸭,to be the apple of your eyes)

❤ star me if you like concent ^_^

相关文章
相关标签/搜索