Pourquoi je préfère me passer de Redux

Pourquoi je préfère me passer de Redux

Alexandre P. dans Dev - Le 29-06-2022

React a changé notre vision du développement applicatif. Depuis sa sortie, cette librairie a connu plusieurs modifications, que ce soit le langage ES5, puis ES6, puis Typescript, etc... Redux avait apporté un brun d'organisation dans toute cette tambouille mais, avec un niveau de complexité non négligeable. Voici une méthode pour y voir plus clair.

ReactJS, un besoin d'organisation

Contrairement à d'autres libraries frontend comme Angular, React donne beaucoup de liberté aux développeurs pour organiser le code. Mais il est difficile de s'y retrouver si plusieurs personnes travaille avec des méthodologies différentes. Redux a apporté une façon d'organiser le code via des événements nommés et connus d'avance. Cependant le code à mettre en place avant de pouvoir le faire est assez dense. Et son utilisation au quotidien peut s'avérer problématique tant il est compliqué d'ajouter de nouveaux événements ou namespaces au fonctionnement existant.

Comment s'organise le code avec Redux

Depuis que les développeurs frontend se sont mis à React, nombreux ont été les tentatives d'organisation du code autour d'un flow qui permet de gérer les états et les changements d'états.

Dans tout cela, Redux a sorti son épingle du jeu en proposant une approche basé sur des stores de données qui écoutent des événements précis afin de changer d'état. Cependant, sa mise en place n'est pas des plus simple. Cela demande une organisation des fichiers sources, la création de chacun des états et la gestion de chacun des événements.

Sans compter le fait que les composants qui utilisent ce store doivent être wrappé dans une fonction qui permettra de déclencher un re-render au moment d'un événement. Et que chaque composant doit être attaché au store pour pouvoir utiliser un état de ce dernier. Bref une mise en place qui une fois qu'on s'est habitué n'est pas si compliqué, mais qui demeure complexe à mettre en place et à organiser.

Chaque nouvel événement implique la création de ce dernier et se doit de spécifier comment va réagir le store de donnée en fonction des événements. Toute cette organisation demande surtout énormément de temps, que finalement on en passera pas à créer des features, mais plutôt à organiser notre code et essayer au maximum de gérer side-effects des événements cross-stores...

redux-flow ><

Le temps passé à mettre en place l'architecture redux vous coûte cher à l'échelle d'un projet complexe.

La mauvaise méthode pour remplacer Redux

La solution à l'ancienne pour impacter un child sans utiliser redux était de créer un state à haut niveau sur un parent element, et passer les getter/setter en props à chaque composants. Ce pattern s'appelle le props-drill pattern.

En revanche, vous imaginez bien les limites de ce modèle, il s'agit de passer toujours de plus en plus de props à chaque composants intermédiaire et ses composants imbriqués.

Pour vous donner une idée :

react-props-drill-pattern ><

Et en implémentation simplifiée cela donnerait quelque chose comme ça :

import * as React from 'react'

type User = {
  name: string
  age: number
}

type ParentContainerProps = {
  children: React.ReactNode
}

// Mock data fetch
const ConnectUser = (cb: (u: User) => void) => {
  cb({
    name: 'John Doe',
    age: 30
  })
}

// parent-container.tsx
const ParentContainer = ({children}: ParentContainerProps) => {
  const [user, setUser] = React.useState<User | null>(null)
  return (
    <div>
      <UserContainer user={user} setUser={setUser} />
      {children}
    </div>
  )
}

// user-container.tsx
const UserContainer = ({ user, setUser }: { 
  user: User|null, 
  setUser: (u: User) => void 
}) => {
  // Fetch data
  React.useEffect(() => {
    ConnectUser((connectedUser: User) => setUser(connectedUser))
  }, [])
  return user ? (
    <div>
      {user.name} - {user.age} ans
    </div>
  ) : null
}

export default ParentContainer

On constate que chaque composant doit avoir les props qui permettent de manipuler un user et se doivent de le passer à chaque child qui en a aussi besoin. Ce pattern est d'autant plus problématique qu'en Typescript, s'il s'agit d'une donnée que l'on récupère de manière asynchrone, cela implique qu'il risque d'être null ou changer d'état.

Une meilleure approche pour remplacer Redux

Depuis que l'on utilise majoritairement des functional components et des hooks, la manière de faire a changé. Désormais, on emploie le Provider/Context pattern.

La mise en place de ce pattern consiste à wrapper le code dans un Provider. Et comme ses enfants viendront consommer son context, il pourra ainsi gérer le re-render des composants si besoin.

Pour rendre mon code réutilisable, je passe par les hooks pour isoler les getters, setters. Ainsi seuls les hooks viendront consommer les contexts (aucune lecture en direct) et les composants consommeront les hooks.

react-provider-context-pattern ><

import * as React from 'react'

type User = {
  id: number 
  username: string
  age: number
  email: string
}

// /context/user-context.tsx
type UserContextValues = {
  connected: boolean
  setConnected: (c: boolean) => void
  userInfos: User
  setUserInfos: (u: User) => void
}

export const UserContext = React.createContext<UserContextValues>({
  connected: false,
  setConnected: () => void,
  userInfos: {
    id: null,
    username: '',
    age: '',
    email: '',
  },
  setUserInfos: () => null,
})

export const UserProvider = ({ children }: { children: React.ReactNode }) => {
  const [connected, setConnected] = React.useState(false)
  const [userInfos, setUserInfos] = React.useState<User>({
    id: null,
    username: '',
    age: '',
    email: '',
  })

  // Get our user
  React.useEffect(() => {
    if (!connected) {
      fetch('/my/user/api/endpoint')
        .then(res => res.json())
        .then(user => {
          setUserInfos(user)
          setConnected(true)
        })
        .catch(err => console.log(err))
    }
  }, [connected])

  return (
    <UserContext.Provider
      value={{
        connected,
        setConnected,
        userInfos,
        setUserInfos,
      }}
    >
      {children}
    </UserContext.Provider>
  )
}

// /hook/use-get-user.tsx
export const useGetUser = () => {
  const { userInfos } = React.useContext(UserContext)
  return userInfos
}

// /components/user-card.tsx
export const UserCard = () => {
  const user = useGetUser()

  return user ? (
    <div className="card">
      <h3>{user.username}</h3>
      <p>{user.age} ans<p>
    </div> 
  ) : null
}

// app.tsx
// ...
return (
  <UserProvider>
    <App />
  </UserProvider>
)

Comme on peut le voir, la deuxième méthode est beaucoup plus propre et malgré un plus grand nombre de fichiers à créer, cela reste une approche simple pour accéder à des données centralisées à la manière d'un redux.

#conseils#programmation#redux#react

user picture
Alexandre P.

Développeur passionné depuis plus de 20 ans, j'ai une appétence particulière pour les défis techniques et changer de technologie ne me fait pas froid aux yeux.