DEV Community

loading...
Cover image for use Concent, release react hook's maximum energy

use Concent, release react hook's maximum energy

fantasticsoul profile image 幻魂 ・9 min read

Hi, my dear friend, I am fantasticsoul, today I want to talk about React's lifecycle method evolution。

If you have any question about Concent, you can read the articles below to know more about it.

Star Concent if you are interested in it, I will appreciate it greatly.

how we manage our effect code before hooks born

Before hook is born, we usually put our effect code in componentDidMount,componentDidUpdate,componentWillUnmount, a typical example may like this:

class SomePage extends Component{
    state = { products: [] }
    componentDidMount(){
        api.fetchProducts()
        .then(products=>this.setState({products}))
        .catch(err=> alert(err.message));
    }
}

If we have many filter condition to query the product, the code may like this:

class SomePage extends Component{
    state = { products: [], type:'', sex:'', addr:'', keyword:'' }

    componentDidMount(){
        this.fetchProducts();
    }

    fetchProducts = ()=>{
        const {type, sex, addr, keyword} = this.state;
        api.fetchProducts({type, sex, addr, keyword})
        .then(products=>this.setState({products}))
        .catch(err=> alert(err.message));
    }

    changeType = (e)=> this.setState({type:e.currentTarget.value})

    changeSex = (e)=> this.setState({sex:e.currentTarget.value})

    changeAddr = (e)=> this.setState({addr:e.currentTarget.value})

    changeKeyword = (e)=> this.setState({keyword:e.currentTarget.value})

    componentDidUpdate(prevProps, prevState){
        const curState = this.state;
        if(
            curState.type!==prevState.type ||
            curState.sex!==prevState.sex || 
            curState.addr!==prevState.addr || 
            curState.keyword!==prevState.keyword 
        ){
            this.fetchProducts();
        }
    }

    componentWillUnmount(){
        // here clear up
    }

    render(){
        const { type, sex, addr, keyword } = this.state;
        return (
            <div className="conditionArea">
                <select value={type} onChange={this.changeType} >{/**some options here*/}</select>
                <select value={sex} onChange={this.changeSex}>{/**some options here*/}</select>
                <input value={addr} onChange={this.changeAddr} />
                <input value={keyword} onChange={this.changeKeyword} />
            </div>
        );
    }
}

And some people don't want so many change*** in code, they will write code like this:

class SomePage extends Component{
    changeKey = (e)=> this.setState({[e.currentTarget.dataset.key]:e.currentTarget.value})

    // ignore other logic......

    render(){
        const { type, sex, addr, keyword } = this.state;
        return (
            <div className="conditionArea">
                <select data-key="type" value={type} onChange={this.changeKey} >
                    {/**some options here*/}
                </select>
                <select data-key="sex" value={sex} onChange={this.changeKey}>
                    {/**some options here*/}
                </select>
                <input data-key="addr" value={addr} onChange={this.changeKey} />
                <input data-key="keyword" value={keyword} onChange={this.changeKey} />
            </div>
        );
    }
}

And if the component will also been updated while some props changed, code may like this:

class SomePage extends Component{
    static getDerivedStateFromProps (props, state) {
        if (props.tag !== state.tag) return {tag: props.tag}
        return null
    }
}

Because componentWillReceiveProps is deprecated, here we use getDerivedStateFromProps instead of componentWillReceiveProps

As a result, we have quickly completed the use of traditional life cycle method, next, we welcome hook to the stage, to see the revolutionary experience it bring us。

With hook, we can write less code to do more things.

Hook tell us forget the confused this in class component, it give you a new way to manage your effect logic, now let rewrite our code with function component.

const FnPage = React.memo(function({ tag:propTag }) {
  const [products, setProducts] = useState([]);
  const [type, setType] = useState("");
  const [sex, setSex] = useState("");
  const [addr, setAddr] = useState("");
  const [keyword, setKeyword] = useState("");
  const [tag, setTag] = useState(propTag);//use propTag as tag's initial value

  const fetchProducts = (type, sex, addr, keyword) =>
    api
      .fetchProducts({ type, sex, addr, keyword })
      .then(products => setProducts(products))
      .catch(err => alert(err.message));

  const changeType = e => setType(e.currentTarget.value);
  const changeSex = e => setSex(e.currentTarget.value);
  const changeAddr = e => setAddr(e.currentTarget.value);
  const changeKeyword = e => setKeyword(e.currentTarget.value);

  // equal componentDidMount&componentDidUpdate
  useEffect(() => {
    fetchProducts(type, sex, addr, keyword);
  }, [type, sex, addr, keyword]);
  // any dependency value change will trigger this callback.

  useEffect(()=>{
      return ()=>{// clear up
          // equal componentWillUnmout
      }
  }, []);//put an zero length array, to let this callback only execute one time after first rendered.

  useEffect(()=>{
     // equal getDerivedStateFromProps
     if(tag !== propTag)setTag(tag);
  }, [propTag, tag]);

  return (
    <div className="conditionArea">
      <select value={type} onChange={changeType}>
        {/**some options here*/}
      </select>
      <select value={sex} onChange={changeSex}>
        {/**some options here*/}
      </select>
      <input value={addr} onChange={changeAddr} />
      <input value={keyword} onChange={changeKeyword} />
    </div>
  );
});

Look at these code above, it is really cool, and the more interesting thing is:a hook can call another hook nested, that means we can put some of our code into a customized hook and reuse it anywhere!

function useMyLogic(propTag){

    // we ignore these code 
    // you just know many useState and useEffect were copied to here
    // .......

    return {
      type, sex, addr, keyword, tag,
      changeType,changeSex,changeAddr, changeKeyword,
    };
}

const FnPage = React.memo(function({ tag: propTag }) {
  const {
    type, sex, addr, keyword, tag,
    changeType,changeSex,changeAddr, changeKeyword,
   } = useMyLogic(propTag);
  // return your ui
});

And if state change process have many steps, we can use useReducer to hold these code, then your customized hook will be more clean.

Here is (Dan Abramov's online example)[https://codesandbox.io/s/xzr480k0np]

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}

Now we see how hook will change our code organization,but is it really enough for us? next let's see how Concent change your hook using way!

With useConcent, release react hook's maximum energy

Before we talk about useConcent(a api supplied by Concent), we point out some shortcomings of hooks。

  • hook will generate many many temporary closure method no matter you care about it or not, it puts a lot of pressure on GC, and at the same time you should use some method like useCallback to avoid some traps or redundant render behavior.
  • useReducer is just a pure function, your async logic code must been placed in your customized hook
  • the hook way is totally different with class lifecycle way, it means function component can not share logic with class component.

Then let's how Concent resolve these 3 problems elegantly!

Let's welcome setup feature to the stage, it will give you a new way to think and write react component.

Here we define a setup function first.

const setup = ctx => {
  console.log('setup will only been executed one time before first render');
  const fetchProducts = () => {
    const { type, sex, addr, keyword } = ctx.state;
    api.fetchProducts({ type, sex, addr, keyword })
      .then(products => ctx.setState({ products }))
      .catch(err => alert(err.message));
  };

  ctx.effect(() => {
    fetchProducts();
  }, ["type", "sex", "addr", "keyword"]);//here only pass state key
  /** equal as:
    useEffect(() => {
      fetchProducts(type, sex, addr, keyword);
    }, [type, sex, addr, keyword]);
  */

  ctx.effect(() => {
    return () => {// clean up
      // equal as componentWillUnmout
    };
  }, []);
  /** Previous writing in function component:
    useEffect(()=>{
      return ()=>{// clean up
        // do some staff
      }
    }, []);
  */

  // attention here, effectProps is reactive to props value change,effect is reactive to state value change
  ctx.effectProps(() => {
    const curTag = ctx.props.tag;
    if (curTag !== ctx.prevProps.tag) ctx.setState({ tag: curTag });
  }, ["tag"]);//only need props key
  /** Previous writing in function component:
  useEffect(()=>{
    if(tag !== propTag)setTag(tag);
  }, [propTag, tag]);
 */

  return {// these return methods will been collected to ctx.settigns
    fetchProducts,
    changeType: ctx.sync('type'),
  };
};

By the way, the more amazing thing about setup is ctx give you another interface like on,computed,watch to enhance your component capability in setup function block, here I give you two online demos.
a standard concent app
about computed&watch

Then we can use the setup function.

import { useConcent } from 'concent';

//state function definition, pass it to useConcent
const iState = () => ({ products:[], type: "", sex: "", addr: "", keyword: "", tag: "" });

const ConcentFnPage = React.memo(function({ tag: propTag }) {
  // useConcent returns ctx,here deconstruct ctx directly
  const { state, settings, sync } = useConcent({ setup, state: iState });
  // attention here we use sync, but if you purchase high performance
  // I suggest you use settings.changeType, or write input like this
  // <input data-ccsync="addr" value={addr} onChange={sync} />
  // because sync('**') will generate a new method in every render period

  const { products, type, sex, addr, keyword, tag } = state;
  const { fetchProducts } = settings;

  // now we can use any method in settings
  return (
    <div className="conditionArea">
      <h1>concent setup compnent</h1>
      <select value={type} onChange={sync('type')}>
        <option value="1">1</option>
        <option value="2">2</option>
      </select>
      <select value={sex} onChange={sync('sex')}>
        <option value="1">male</option>
        <option value="0">female</option>
      </select>
      <input value={addr} onChange={sync('addr')} />
      <input value={keyword} onChange={sync('keyword')} />
      <button onClick={fetchProducts}>refresh</button>
      {products.map((v, idx)=><div key={idx}>name:{v.name} author:{v.author}</div>)}
    </div>
  );
});

Setup allows you define static method, that means your component's every render period will not generate so many temporary closure function and call some many use***, let's see the effect below:

Up to now we resolve the first problem: many many temporary closure method generated in every render period. how can we resolve the second problem: separate the async logic code to a single file(we can call it logic file)

Use invoke can easily do this, let's see how it works.

//code in logic.js
export async function complexUpdate(type, moduleState, actionCtx){
    await api.updateType(type);
    return { type };
}

// code in setup function
import * as lc from './logic';

const setup = ctx=>{
    //other code ignored....
    return {
        upateType: e=> ctx.invoke(lc.complexUpdate, e.currentTarget.value);
    }
}

Is this more cute to write and read? you may see the third param actionCtx in function params list, it can allow you combine other function easily.

//code in logic.js
export async function complexUpdateType(type, moduleState, actionCtx){
    await api.updateType(type);
    return { type };
}

export async function complexUpdateSex(sex, moduleState, actionCtx){
    await api.updateSex(sex);
    return { sex };
}

export async function updateTypeAndSex({type, sex}, moduleState, actionCtx){
    await actionCtx.invoke(complexUpdateType, type);
    await actionCtx.invoke(complexUpdateSex, sex);
}

// code in setup function
import * as lc from './logic';

const setup = ctx=>{
    //other code ignored....
    return {
        upateType: e=> {
            // 为了配合这个演示,我们另开两个key存type,sex^_^
            const {tmpType, tmpSex} = ctx.state;
            ctx.invoke(lc.updateTypeAndSex, {type:tmpType, sex:tmpSex}};
        }
    }
}

I believe using this way to write code is more readable and maintainable,and you may ask we pass a state definition function to useConcent, it is a private state for the function Component, how we promote it to a shared state.

Yeah, if you have this question, you asked the right person, Concent can do it very quickly and easily, with a very less code changes。

setup 1, config modules

import { useConcent, run } from "concent";
import * as lc from './logic';

run({
    product:{
        state: iState(), 
        // here we can not config lc as reducer
        // but I suggest you do it, then you can call method with ctx.moduleReducer.xxx directly
        reducer: lc,
    }
});

setup 2, pass moudle name to useConcent

const ConcentFnModulePage = React.memo(function({ tag: propTag }) {
  // attention, here we don't pass the state to it any more, just flag current component belong to product module, then Concent will inject all product module's state to ctx.state
  const { state, settings, sync } = useConcent({ setup, module:'product' });
  const { products, type, sex, addr, keyword, tag } = state;
  const { fetchProducts } = settings;

  // code ignored here, they are 100% the same....
  );
});

So now we have 2 components, one is ConcentFnPage, the other is ConcentFnModulePage, ConcentFnPage can still work very well, the little different in code level between these 2 components is ConcentFnPage have own private state, ConcentFnModulePage mark module as product, so all the instances of ConcentFnModulePage will share the state! let's initialize 2 instance of ConcentFnPage and 2 instance of ConcentFnModulePage, and see the effect below:

The problem 2 is resolved, and we only remain the last problem: how can the function component and class component share the logic?

I'm so proud to announce that setup is also can be used on class component, so the last problem is not a problem any more, let me show you the code:

class ConcentFnModuleClass extends React.Component{
  render(){
    const { state, settings, sync } = this.ctx;
    const { products, type, sex, addr, keyword, tag } = state;
    const { fetchProducts, fetchByInfoke } = settings;

    // code ignored here, they are 100% the same....
  }
}

export default register({ setup, module:'product' })(ConcentFnModuleClass);

let's see the effect, be attention that all the instances shared one module's state:

end

I know some of you still don't believe what happed above or want to try it self, so here is the online example link, welcome to fork and change.
https://codesandbox.io/s/nifty-cdn-6g3hh

More details see Concent git repo
or see Concent official doc

Concent is a predictable、zero-cost-use、progressive、high performance's enhanced state management solution, star it if you are interesting in the way I tell you above, thank you so much.

Discussion (0)

Forem Open with the Forem app