The history of React component reuse

Mixins

React Mixin is implemented by wrapping shared methods into Mixins methods, and then injecting each component. It is officially deprecated, but you can still learn and understand why it was abandoned.

React MiXin can only be used through React.createClass(), as follows:

const mixinDefaultProps = {}
const ExampleComponent = React.createClasss({
  mixins: [mixinDefaultProps],
  render: function(){}
})

Mixins implementation

import React from 'react'

var createReactClass = require('create-react-class')

const mixins = {
  onMouseMove: function(e){
    this.setState({
      x: e.clientX,
      y: e.clientY
    })
  }
}

const Mouse = createReactClass({
  mixins: [mixins],
  getInitialState: function() {
    return {
      x: 0,
      y: 0
    }
  },
  render() {
    return (<div onMouseMove={this.onMouseMove} style={{height: '300px'}}>
      <p>the current mouse position is ({this.state.x},{this.state.y})</p>
    </div>)
  }
})

Mixins problem

  • Mixins introduce implicit dependencies

You might write a stateful component, and then your colleagues might add a mixin that reads the state of this component. After a few months, you may want to move that state to the parent component so that it can be shared with its siblings. Will you remember to update this mixin to read props instead of state? What if other components are also using this mixin at this time?

  • Mixins cause name clashes

There is no guarantee that two specific mixin s will work together. For example, if FluxListenerMixin and WindowSizeMixin are both defined to handleChange(), they cannot be used together. Also, you cannot define a method with this name on your own component.

  • Mixins lead to snowballing complexity

Each new requirement makes mixins harder to understand. Over time, more and more components use the same mixin. New functionality for any mixin is added to all components that use that mixin. There is no way to split the "simpler" parts of a mixin, unless or by introducing more dependencies and indirection. Gradually, the boundaries of encapsulation are eroded, and existing mixins are constantly becoming more abstract as it is difficult to change or remove them, until no one understands how they work.

Related React actual combat video explanation: into learning

higher order components

Higher-order components (HOC) are an advanced technique for reusing component logic in React. HOC itself is not part of the React API, it is a design pattern based on React's compositional features.

A higher-order component is a function whose parameter is a component and whose return value is a new component

A component is what transforms props into UI, and a higher-order component is what transforms a component into another component.

const EnhancedComponent = higherOrderComponent(WrappedComponent)

Implementation of HOC

  • Props Proxy: HOC operates on props passed to WrappedComponent
  • Inheritance Inversion HOC inherits WrappedComponent, officially not recommended

Props Proxy

import React from 'react'

class Mouse extends React.Component {
  render() {
    const { x, y } = this.props.mouse 
    return (
      <p>The current mouse position is ({x}, {y})</p>
    )
  }
}

class Cat extends React.Component {
  render() {
    const { x, y } = this.props.mouse 
    return (<div style={{position: 'absolute', left: x, top: y, backgroundColor: 'yellow',}}>i am a cat</div>)
  }
}

const MouseHoc = (MouseComponent) => {
  return class extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        x: 0,
        y: 0
      }
    }
    onMouseMove = (e) => {
      this.setState({
        x: e.clientX,
        y: e.clientY
      })
    }
    render() {
      return (
        <div style={{height: '300px'}} onMouseMove={this.onMouseMove}>
          <MouseComponent mouse={this.state}/>
        </div>
      )

    }
  }
}

const WithCat = MouseHoc(Cat)
const WithMouse = MouseHoc(Mouse)

const MouseTracker = () => {
    return (
      <div>
        <WithCat/>
        <WithMouse/>
      </div>
    )
}

export default MouseTracker

Please note: HOC does not modify incoming components, nor does it use inheritance to replicate its behavior. Instead, HOC composes new components by wrapping them in container components. HOC s are pure functions with no side effects.

What can we do in Props Proxy mode?

  • Operation Props

In the HOC, you can add, delete, modify, and check the props, as follows:

  const MouseHoc = (MouseComponent, props) => {
    props.text = props.text + '--I can operate props'
   return class extends React.Component {
      render() {
        return (
          <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
            <MouseComponent {...props} mouse={this.state} />
          </div>
        )
      }
  }

  MouseHoc(Mouse, {
    text: 'some thing...'
  })
  • Access components via Refs
  const MouseHoc = (MouseComponent) => {
    return class extends React.Component {
      ...
      render() {
        const props = { ...this.props, mouse: this.state }
        return (
          <div style={{height: '300px'}} onMouseMove={this.onMouseMove}>
            <MouseComponent {...props}/>
          </div>
        )
      }
    }
  }

  class Mouse extends React.Component {
    componentDidMounted() {
      this.props.onRef(this)
    }
    render() {
      const { x, y } = this.props.mouse 
      return (
        <p>The current mouse position is ({x}, {y})</p>
      )
    }
  }

  const WithMouse = MouseHoc(Mouse)

  class MouseTracker extends React.Component {
    onRef(WrappedComponent) {
      console.log(WrappedComponent)// Mouse Instance
    }
    render() {
      return (
        <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
          <WithMouse mouse={this.state} ref={this.onRef}/>
        </div>
      )
    }
  }
  • extract state
  <MouseComponent mouse={this.state}/>
  • Wrap WrappedComponent
  <div style={{height: '300px'}} onMouseMove={this.onMouseMove}>
    <MouseComponent {...props}/>
  </div>

Inheritance Inversion

This pattern is relatively rare, a simple example is as follows:

  function iiHOC(WrappedComponent) {
    return class WithHoc extends WrappedComponent {
      render() {
        return super.render()
      }
    }
  }

Inheritance Inversion allows HOC to access WrappedComponent through this, which means that it can access state, props, component lifecycle methods, and render methods. HOC can add, delete, modify, and check the state of WrappedComponent instances, which will cause confusion in state relationships and prone to bug s. To restrict the HOC from reading or adding state, adding state should be placed in a separate namespace, not together with WrappedComponent's state

class Mouse extends React.Component {
  render(props) {
    const { x, y } = props
    return (
      <p>The current mouse position is ({x}, {y})</p>
    )
  }
}

const MouseHoc = (MouseComponent) => {
  return class extends MouseComponent {
    constructor(props) {
      super(props)
      this.state = {
        x: 0,
        y: 0
      }
    }
    onMouseMove = (e) => {
      this.setState({
        x: e.clientX,
        y: e.clientY
      })
    }
    render() {
      const props = { mouse: this.state }
      return (
        <div style={{height: '300px'}} onMouseMove={this.onMouseMove}>
          {super.render(props)}
        </div>
      )
    }
  }
}

const WithMouse = MouseHoc(Mouse)

HOC convention

  • Pass irrelevant props to wrapped components

HOC s add features to components. It shouldn't drastically change the convention itself. The component returned by the HOC should maintain a similar interface to the original component.

HOC should transparently transmit props that have nothing to do with itself. Most HOC s should contain a render method similar to the following:

  render() {
    // Filter out props properties specific to this higher-order component, and do not pass through
    const { extraProp, ...passThroughProps } = this.props

    // Inject props into the wrapped component
    // Usually a value of state or an instance method
    const injectedProp = someStateOrInstanceMethod

    // Pass props to the wrapped component
    return (
      <WrappedComponent
        injectedProp = {injectedProp}
        {...passThroughProps}      />
    )

  }

This agreement guarantees the flexibility and reusability of the HOC.

  • Maximize composability

Not all HOC s are the same, sometimes it takes only one parameter, the wrapped component:

  const NavbarWithRouter = withRouter(Navbar)

HOC s can usually receive multiple parameters. For example, in Relay, HOC additionally receives a configuration object to specify component data dependencies:

  const CommentWithRelay = Relay.createContainer(Comment, config)

The most common HOC signatures are as follows:

// React Redux's `connect` function
const ConnectedComment = connect(commentSelector, commentActions)(CommentList)

// Take it apart
// connect is a function whose return value is another function
const enhance = connect(commentListSelector, commentListActions)
// The return value is HOC, which will return the component connected to the Redux store
const ConnectedComment = enhance(CommentList)

In other words, connect is a higher-order function that returns a higher-order component.

This form may seem confusing or unnecessary, but it has a useful property. A single-argument HOC like the one returned by the connect function has the signature Component => Component. Functions with the same output type as the input type are easily grouped together.

// instead of this
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// You can write combined utility functions
const enhance = compose(withRouter, connect(commentSelector))
const EnhancedComponent = enhance(WrappedComponent)
  • Wrap display name for easy debugging

Container components created by HOC s appear in React Developer Tools just like any other component. In order to facilitate debugging, please select a display name, which has been indicated as a HOC product.

For example, the name of the higher-order component is withSubscription, the display name of the packaged component is CommentList, and the display name should be WithSubscription(CommentList)

  function withSubscription(WrappedComponent) {
    class WithSubscription extends React.Component {/*....*/}
    WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`
    return WithSubscription
  }

  function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component'
  }

Precautions

  • Don't use HOC in render method
  render() {
    // A new EnhancedComponent is created every time the render function is called
    // EnhancedComponent1 !== EnhancedComponent2
    const EnhancedComponent = enhance(MyComponent)
    // This will cause the subtree to be unloaded and remounted every time it is rendered
    return <EnhancedComponent/>
  }
  • Always copy static methods

When you apply a HOC to a component, the original component will be wrapped with a container component, which means the new component doesn't have any static methods of the original component.

  // define static method
  WrappedComponent.staticMethod = function(){/*...*/}
  // Now use HOC
  const EnhancedComponent = enhance(WrappedComponent)

  // Enhanced components do not have staticMethod
  typeof EnhancedComponent.staticMethod === 'undefined' // true

To solve this problem, you can copy these methods to the container component before returning:

  function enhance(WrappedComponent) {
    class Enhance extends React.Component {/*...*/}
    // Must know exactly which methods should be copied
    Enhance.staticMethod = WrappedComponent.staticMethod
    return Enhance
  }

But to do this, you need to know which methods should be copied, you can use hoist-non-react-statics to automatically copy all React static methods:

  import hoistNonReactStatic from 'hoist-non-react-statics'
  function enhance(WrappedComponent) {
    class Enhance extends React.Component {/*..*/}
    hoistNonReactStatic(Enhance, WrappedComponent)
    return Enhance
  }

In addition to exporting components, another feasible solution is to additionally export this static method

  MyComponent.someFunction = someFunction
  export default MyComponent

  // ...export the method separately
  export { someFunction }

  // ...and in the components you want to use, import them
  import MyComponent, { someFunction } form './Mycomponent.js'
  • Refs will not be passed

While the HOC convention is to pass all props to the wrapped component, that doesn't apply to refs. Because ref is not actually a prop, like key, it is handled specially by React. If you add a ref to the HOC's return component, the ref references point to the container component, not the wrapped component.

Render Props

"render prop" refers to a simple technique for sharing code between React components using a prop whose value is a function.

A component with a render prop accepts a function that returns a React element and calls it instead of implementing its own rendering logic

  <DataProvider render={data => (
    <h1>Hello {data.target}</h1>
  )}/>

Render Props implementation

render props is a function prop that tells the component what to render

class Cat extends React.Component {
  render() {
    const { x, y } = this.props.mouse 
    return (<div style={{position: 'absolute', left: x, top: y, backgroundColor: 'yellow',}}>i am a cat</div>)
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      x: 0,
      y: 0
    }
  }
  onMouseMove = (e) => {
    this.setState({
      x: e.clientX,
      y: e.clientY
    })
  }
  render() {
    return (
      <div style={{height: '300px'}} onMouseMove={this.onMouseMove}>
        {this.props.render(this.state)}
      </div>
    )
  }
}

export default class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <Mouse render={mouse => {
          return <Cat mouse={mouse}/>
        }}/>
      </div>
    )
  }
}

Interestingly, you can implement most HOC s using regular components with a render prop.

Note: You don't have to use a prop named render to use this pattern. In fact, any function prop that is used to tell a component what to render can technically be called a "render prop".

Although the previous example uses render, we can simply use the children prop!

<Mouse children={mouse => (
  <p>mouse position {mouse.x}, {mouse.y}</p>
)}/>

Remember, the children prop doesn't really need to be added to the "attributes" list of the JSX element. You can put it directly inside the element!

<Mouse>
 {mouse => (
  <p>mouse position {mouse.x}, {mouse.y}</p>
  )}
</Mouse>

Due to the specificity of this technique, when you are dealing with a similar API, it is recommended to declare that the type of children should be a function in your propTypes.

  Mouse.propTypes = {
    children: PropTypes.func.isRequired
  }

Be careful when using Render props with React.PureComponent

  class Mouse extends React.PureComponent {
    // ...
  }

  class MouseTracker extends React.Component {
    render() {
      return (
        <div>
          {
            // this is not good! The value of the `render`prop will be different for each render
          }
          <Mouse render={mouse => {
            <Cat mouse={mouse}/>
          }}/>
        </div>
      )
    }
  } 

In the above example, every time <MouseTracker> renders, it will generate a new function as a prop of <Mouse render>, so at the same time cancel the effect of the <Mouse> component which inherits from React.PureComponent.

You can define a prop as an instance method:

  class MouseTracker extends React.Component {
    renderTheCat(mouse) {
      return <Cat mouse={mouse}/>
    }
    render() {
      return (
        <div>
          <Mouse render={this.renderTheCat}/>
        </div>
      )
    }
  } 

Higher order components and render props issues

  • Difficult to reuse logic, resulting in a deep component tree hierarchy

If you use the HOC or render props scheme to reuse state logic between components, it will easily form "nested hell".

  • Business logic is scattered across the various methods of the component
class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

As the functionality of the application expands, the components will become more complex and will gradually become flooded with state logic and side effects. Each lifecycle often contains some unrelated logic. For example, in the above code, the logic of setting document.title is divided into componentDidMount and componentDidUpdate, and the subscription logic is divided into componentDidMount and componentWillUnmount. And componentDidMount contains code for two different functions at the same time.

  • incomprehensible class

You need to learn class syntax and understand how this works in Javascript, which is very different from other languages. Also can't forget to bind event handling. There is also disagreement about the difference between function composition and class components, and even the usage scenarios of the two components. The use of class components inadvertently encourages developers to use schemes that make optimizations ineffective. Classes also cause problems with current tools, for example, class es do not compress well and can make hot reloading unstable.

Hooks

Hook s are a new feature in React 16.8 that allow you to use state and other React features without writing class es.

Hooks implementation

Using State Hooks

import React, { useState } from 'react'

function Example() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>you clicked {count} times</p>
      <button onClick={() => setCount(count+1)}>        Click me      </button>
    </div>
  )
}

Declare multiple state variables

function ExampleWithManyStates() {
  // Declare multiple state variables
  const [age, setAge] = useState(42)
  const [fruit, setFruit] = useState('banana')
  const [todos, setTodos] = useState([{text: 'Learn Hooks'}])
}

What do you do when you call the useState method?

It defines a "state variable". We can call it any name, and the function provided by this.state in the class is exactly the same.

What parameters does useState require?

The only parameter in the useState() method is the initial state, which can be a number or a string, not necessarily an object.

What is the return value of the useState method?

The return value is: the current state and the function that updates the state.

Using Effect Hook s

Effect Hook s allow you to perform side effects in function components

Data fetching, setting subscriptions, and manually changing the DOM in React components are all side effects.

You can think of useEffect Hook as a combination of three functions: componentDidMount,componentDidUpdate and componentWillUnmount. In React components, there are two common side-effect actions: those that need to be cleaned up and those that don't.

  • No need to clean up effect s

Sometimes we just want to run some extra code after React has updated the DOM. For example, sending network requests, manually changing the DOM, and recording logs are common operations that do not need to be cleaned up. Because after we have performed these operations, we can ignore them.

import React, { useState, useEffect } from 'react'

function Example() {
  const [count, setCount] = useState(0)

  // Similar to componentDidMount and componentDidUpdate
  useEffect(() => {
    // Update document title using browser API
    document.title = `You clicked ${count} times`
  })

  return (
    <div>
      <p>you clicked {count} times</p>
      <button onClick={() => setCount(count+1)}>        Click me      </button>
    </div>
  )
}

What does useEffect do?

By using this Hook, you can tell the React component that it needs to do something after rendering. React will save the function you pass and call it after performing the DOM update.

Why useEffect is called inside a component

Putting useEffect inside the component gives us direct access to the countstate variable (or other props) in the effect. Here Hook uses JavaScript's closure mechanism.

Will useEffect be executed after every render?

Yes, by default it executes after the first render and after every update.

useEffect function will be different in every render?

Yes, this is intentional. In fact, this is why we deliberately get the latest count value in the effect without worrying about expiration. Because every time we re-render, a new effect will be generated, replacing the previous one. In a sense, effects are more like part of the rendering result - each effect "belongs" to a specific rendering.

Tip: Unlike componentDidMount or componentDidUpdate, effect s dispatched with useEffect do not block the browser from updating the screen, which makes your application appear more responsive. In most cases, effect s do not need to be executed synchronously. In individual cases (such as measuring layouts), there is a separate useLayoutEffectHook for you to use with the same API as useEffect.

  • Effects that need to be cleaned up

For example, subscribing to an external data source, in this case, the cleanup work is very important to prevent memory leaks.

function Example() {
  const [count, setCount] = useState(0)
  const [width, setWidth] = useState(document.documentElement.clientWidth)

  useEffect(() => {
    document.title = `You clicked ${count} times`
  })

  useEffect(() => {
    function handleResize() {
      setWidth(document.documentElement.clientWidth)
    }
    window.addEventListener('resize', handleResize)
    return function cleanup() {
      window.removeEventListener('resize', handleResize)
    }
  })

  return (
    <div>
      <p>you clicked {count} times</p>
      <button onClick={() => setCount(count+1)}>        Click me      </button>
      <p>screen width</p>
      <p>{width}</p>
    </div>
  )
}

Why return a function in an effect?

This is an optional cleanup mechanism for effect s. Each effect can return a cleanup function, so the logic for adding and removing subscriptions can be put together. They are all part of the effect.

When does React clear effect s?

React will perform a cleanup operation when the component is unmounted. The effect is executed every time it is rendered, and the previous effect is cleared before the current effect is executed.

Note: You don't have to name the function returned in the effect, you can return an arrow function or have another name.

Why do you have to run Effect every time you update

The following is a FriendStatus component used to display whether a friend is online. Read friend.id from the props in the class, then subscribe to the friend's status after the component is mounted, and unsubscribe when the component is unloaded.

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend,id,
      this.handleStatusChange
    )
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeToFriendStatus(
      this.props.friend,id,
      this.handleStatusChange
    )
  }

But what happens when the friend prop changes when the component is already on the screen? Our component will continue to display the old friend status, which is a bug. And we also have memory leaks or crashes due to using the wrong friend ID when unsubscribing.

In the class component, we need to add componentDidUpdate to solve this problem.

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend,id,
      this.handleStatusChange
    )
  }
  componentDidUpdate(prevProps) {
    // friend.id before unsubscribing
    ChatAPI.unsubscribeToFriendStatus(
      this.props.friend,id,
      this.handleStatusChange
    )
    // subscribe new friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend,id,
      this.handleStatusChange
    )
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeToFriendStatus(
      this.props.friend,id,
      this.handleStatusChange
    )
  }

If using Hook:

function FriendStatus(props) {
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
    return () => {
      ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange)
    }
  })
}

It is not affected by this bug, because useEffect handles it by default. It cleans up the previous effect before calling a new one. The specific calling sequence is as follows:

// Mount with { friend: {id: 100}} props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange) // run the first effect

// Update with { friend: {id: 200}} props
ChatAPI.unsubscribeToFriendStatus(100, handleStatusChange) // clear the previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange) // run the next effect

// Update with { friend: {id: 300}} props
ChatAPI.unsubscribeToFriendStatus(200, handleStatusChange) // clear the previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange) // run the next effect

// Unmount
ChatAPI.unsubscribeToFriendStatus(200, handleStatusChange) // clear the last effect

Performance optimization by skipping Effect s

In some cases, performing cleanup or executing effect s after each render can cause performance issues. In the class component, we can solve it by adding comparison logic to prevProps or prevState in componentDidUpdate:

  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
      document.title = `You clicked ${count} times`
    }
  }

For useEffect, just pass an array as the second optional parameter of useEffect:

  useEffect(() => {
    document.title = `You clicked ${count} times`
  }, [count])

If the count does not change when the component is re-rendered, React will skip this effect, thus achieving performance optimization. If there are multiple elements in the array, React will execute the effect even if only one element changes.

The same applies to effect s with cleanup operations:

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline)
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatuschange)
    return () => {
      ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatuschange)
    }
  }, [props.friend.id]) // Resubscribe only when props.friend.id changes

Note: If you want to execute an effect that runs only once (only when the component is mounted and unmounted), you can pass an empty array ([]) as the second parameter.

Hook rules

  • Only use hooks at the top level

Do not call Hooks in loops, conditionals or nested functions, this ensures that Hooks are called in the same order on every render. This allows React to keep the hook state correct across multiple useState and useEffect calls.

  • Use Hook s only in React functions

Don't call Hook s in normal Javascript functions

Custom Hook

With custom Hook s, component logic can be extracted into reusable functions.

For example, we have the following components to display the online status of friends:

import React, { useState, useEffect } from 'react'

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null)

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline)
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
    return () => {
      ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange)
    }
  })
  if(isOnline === null) {
    return 'Loading...'
  }
  return isOnline ? 'Online' : 'Offline'
}

Now suppose there is a list of contacts in the chat application, and when the user is online, the name is set to green. We could copy and paste similar logic into the FriendListItem component, but this is not an ideal solution:

import React, { useState, useEffect } from 'react'

function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null)

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline)
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
    return () => {
      ChatAPI.unsubscribeToFriendStatus(props.friend.id, handleStatusChange)
    }
  })
  return (
    <li style={{ color: isOnline ? 'green': 'black'}}>
    {props.friend.name}    </li>
  )
}

Extract custom Hook

When we want to share logic between two functions, we extract it into a third function. Components and Hook s are both functions, so this approach is also applicable.

A custom Hook is a function whose name starts with "use", and other Hooks can be called inside the function.

import React, { useState, useEffect } from 'react'

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null)

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline)
    }
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange)
    return () => {
      ChatAPI.unsubscribeToFriendStatus(friendID, handleStatusChange)
    }
  })
  return isOnline
}

So, the previous FriendStatus and FriendListItem components can be rewritten as follows:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id)
  if(isOnline === null) {
    return 'Loading...'
  }
  return isOnline ? 'Online' : 'Offline'
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id)
  return (
    <li style={{ color: isOnline ? 'green': 'black'}}>
    {props.friend.name}    </li>
  )
}

Is this code equivalent to the original sample code?

Equivalent, it works exactly the same way. Custom Hooks are a convention that naturally follows Hook design, not a React feature

Do custom hooks have to start with "use"?

It must be so. This convention is very important. If you don't follow it, React will not be able to automatically check whether your Hook violates the rules of Hook because it can't tell whether a function contains a call to its internal Hook.

Will using the same Hook in two components share state?

Won't. Every time a custom Hook is used, all state and side effects in it are completely isolated.

Principle of React Hooks

Pseudocode above:

useState

import React from 'react'
import ReactDOM from 'react-dom'

let _state

function useState(initialValue) {
  _state = _state || initialValue

  function setState(newState) {
    _state = newState
    render()
  }
  return [_state, setState]
}

function App() {
  let [count, setCount] = useState(0)
  return (
    <div>
      <div>{count}</div>
      <button onClick={() => {         setCount(count + 1)      }}>click</button>
    </div>
  )
}

const rootElement = document.getElementById('root')

function render() {
  ReactDOM.render(<App/>, rootElement)
}

render()

useEffect

let _deps

function useEffect(callback, depArray) {
  const hasChangedDeps = _deps ? !depArray.every((el, i) => el === _deps[i]) : true
  if (!depArray || hasChangedDeps) {
    callback()
    _deps = depArray
  }
}

useEffect(() => {
  console.log(count)
})

Not Magic, just Arrays

Although the above code implements useState and useEffect that can work, they can only be used once. for example:

const [count, setCount] = useState(0)
const [username, setUsername] = useState('fan')

count and usename are always equal, because they share a _state, so we need to be able to store multiple _state and _deps. We can use arrays to solve the reuse problem of Hooks.

If all _state and _deps are stored in an array, we need a pointer to identify which value is currently taken.

import React from 'react'
import ReactDOM from 'react-dom'

let memorizedState = []
let cursor = 0  //pointer

function useState(initialValue) {
  memorizedState[cursor] = memorizedState[cursor] || initialValue
  const currentCursor = cursor
  function setState(newState) {
    memorizedState[currentCursor] = newState
    render()
  }
  return [memorizedState[cursor++], setState]
}

function useEffect(callback, depArray) {
  const hasChangedDeps = memorizedState[cursor] ? !depArray.every((el, i) => el === memorizedState[cursor][i]) : true
  if (!depArray || hasChangedDeps) {
    callback()
    memorizedState[cursor] = depArray
  }
  cursor++
}

function App() {
  let [count, setCount] = useState(0)
  const [username, setUsername] = useState('hello world')
  useEffect(() => {
    console.log(count)
  }, [count])
  useEffect(() => {
    console.log(username)
  }, [])
  return (
    <div>
      <div>{count}</div>
      <button onClick={() => { 
        setCount(count + 1)
      }}>click</button>
    </div>
  )
}

const rootElement = document.getElementById('root')

function render() {
  cursor = 0
  ReactDOM.render(<App/>, rootElement)
}

render()

At this point, we can implement an arbitrary reuse of useState and useEffect.

Tags: React

Posted by Danestar on Mon, 03 Oct 2022 09:12:21 +0530