Published on

Functional vs class components

Authors

Class vs functionnal component

Pouvoir utiliser les hooks react (v16.8) et custom dans nos composants.

Sur certains projets il m'arrive encore de tomber sur des composants react sous formes de classe. J'en profite pour montrer une refacto en fonction. Ici ce composant est un container qui utilise redux ainsi que react-router-dom avec une certaine logique afin de rendre un composant visuel.

Prennons en exemple ce composant InnerLayoutCContainer.

Class component:

class InnerLayoutContainer extends React.Component {
  componentDidMount() {
    this.initLanguage()
  }

  componentDidUpdate = (prevProps) => {
    const { location, menuOpen, actions } = this.props
    if (location.pathname !== prevProps.location.pathname) {
      if (menuOpen) {
        actions.toggleMenu()
      }
    }
  }

  static contextType = configContext

  initLanguage() {
    const { config } = this.props

    i18next.changeLanguage(config.global.lang.toLowerCase())
  }

  render() {
    const { location } = this.props
    const adminView = location.pathname === routes.ADMIN.path

    return <InnerLayout adminView={adminView} {...this.props} />
  }
}

const mapStateToProps = (state, ownProps) => ({
  menuOpen: state.layout.menuOpen,
  isClient: ownProps.location.pathname.split('/')[1] === routes.CLIENT_DASHBOARD.path.split('/')[1],
  whoami: state.authentication.whoami,
  getWhoamiHasInit: state.authentication.getWhoamiHasInit,
  getWhoamiPending: state.authentication.getWhoamiPending,
  config: state.authentication.config,
})
const mapDispatchToProps = (dispatch) => ({
  actions: {
    toggleMenu: () => dispatch(toggleMenu()),
    toggleDebugMode: () => dispatch(toggleDebugMode()),
  },
})

export default compose(
  withStyles(style, { withTheme: true }),
  withRouter,
  withTranslation(),
  connect(mapStateToProps, mapDispatchToProps)
)(InnerLayoutContainer)

Son refacto en function component:

const InnerLayoutContainer = () => {
  const location = useLocation()
  const dispatch = useDispatch()
  const locationRef = useRef(location.pathname)
  const menuOpen = useSelector((state) => state.layout.menuOpen)
  const auth = useSelector((state) => state.authentication)
  const language = useConfig('global')?.lang?.toLowerCase()
  const { i18n } = useTranslation()

  const isClient = location.pathname.split('/')[1] === routes.CLIENT_DASHBOARD.path.split('/')[1]
  const adminView = location.pathname === routes.ADMIN.path
  const innerLayoutProps = {
    menuOpen,
    isClient,
    adminView,
    whoami: auth.whoami,
    getWhoamiHasInit: auth.getWhoamiHasInit,
    getWhoamiPending: auth.getWhoamiPending,
  }

  useEffect(() => {
    i18n.changeLanguage(language)
  }, [])
  useEffect(() => {
    locationRef.current = location.pathname
  }, [location.pathname])

  const prevLocation = locationRef.current

  useEffect(() => {
    if (location.pathname !== prevLocation && menuOpen) {
      dispatch(toggleMenu())
    }
  }, [location, prevLocation])

  return <InnerLayout adminView={adminView} {...innerLayoutProps} />
}

export default InnerLayoutContainer

useRef

Avec un functionnal component nous n’avons plus accès aux prevProps via le cycle de vie qu'avait les classes, en l'occcurence componentDidUpdate. Ici un des moyens d’imiter le comportement de ces prevProps est d’utiliser le hook react useRef().

De base useRef() et sa propriété .current ne va jamais changer de valeur à travers les renders de nos composants. .current. étant mutable il va donc être possible de lui assigner une valeur et de pouvoir l'utiliser après le prochain render sans que celle-ci soit réinitilisée.

On initialise la ref avec par défaut l'url actuelle:

const location = useLocation()
const locationRef = useRef(location.pathname)

A chaque changement d'url (location.pathname) on attribue l'url actuelle à .current.

useEffect(() => {
  locationRef.current = location.pathname
}, [location.pathname])

const prevLocation = locationRef.current

Enfin, on peut maintenant faire la différence entre l'ancienne (prevLocation) et la nouvelle (location.pathname) url afin d'exécuter une certaine action, dans notre cas plier/déplier un menu.

useEffect(() => {
  if (location.pathname !== prevLocation && menuOpen) {
    dispatch(toggleMenu())
  }
}, [location, prevLocation])

Redux

Pour la partie redux, sous forme de classe, 2 fonctions sont créées en dehors du composants, mapDispatchToProps (dispatch) et mapStateToProps (state). on les passe enuiste en tant qu'arguments à connect() afin de pouvoir les utiliser dans notre classe et ainsi lier notre store redux à notre composant.

// Class component
const mapStateToProps = (state, ownProps) => ({
  menuOpen: state.layout.menuOpen,
  isClient: ownProps.location.pathname.split('/')[1] === routes.CLIENT_DASHBOARD.path.split('/')[1],
  whoami: state.authentication.whoami,
  getWhoamiHasInit: state.authentication.getWhoamiHasInit,
  getWhoamiPending: state.authentication.getWhoamiPending,
  config: state.authentication.config,
})
const mapDispatchToProps = (dispatch) => ({
  actions: {
    toggleMenu: () => dispatch(toggleMenu()),
    toggleDebugMode: () => dispatch(toggleDebugMode()),
  },
})

export default compose(connect(mapStateToProps, mapDispatchToProps))(InnerLayoutContainer)

En ce qui concerne notre function component on utilise 2 hooks qui proviennent de l'api Redux. On retrouve ici l'équivalent (ou presque) du code d'au dessus, cette fois le scope de notre composant:

// Function component
const dispatch = useDispatch()
const menuOpen = useSelector((state) => state.layout.menuOpen)
const auth = useSelector((state) => state.authentication)

Cycle de vie

Avec cet exemple on voit que useEffect() remplace les différentes méthodes componentDidMount, componentDidUpdate ou componentWillUnmount (non présenté ici) afin de gérer les différents effet de bords.

Ce code par exemple peut être un équivalent à componentDidMount(). Il ne s'éxécutera qu'une fois après le premier render.

useEffect(() => {
  i18n.changeLanguage(language)
  // On souhaite changer le langage seulement une fois
  // Tableau de dépendences vide
}, [])

Conclusion

Au final le vrai but des functional component introduit avec react v16.8 est d'éviter la duplication de code grâce aux custom hooks mais aussi de rendre plus clair la lisibilité des composants. Un useEffect() -> une responsabilité isolée (separation of concern). Et non pas un sujet dispatché entre les méthodes componentDidMount componentDidUpdate comme cela peut être le cas avec une classe.