Maison  >  Article  >  interface Web  >  L'évolution de la gestion des états React : du local à l'asynchrone

L'évolution de la gestion des états React : du local à l'asynchrone

WBOY
WBOYoriginal
2024-08-21 06:49:021135parcourir

Table des matières

  • Introduction
  • État local
    • Composants de classe
    • Composants fonctionnels
    • utiliser le crochet réducteur
  • État global
    • Qu’est-ce que l’État global ?
    • Comment l'utiliser ?
    • La voie principale
    • La manière simple
    • La mauvaise façon
  • État asynchrone
  • Conclusion

Introduction

Salut !

Cet article présente un aperçu de la façon dont L'État était géré dans les applications React il y a des milliers d'années, lorsque les composants de classe dominaient le monde et que les composants fonctionnels n'étaient qu'une idée audacieuse, jusqu'à récemment. , lorsqu'un nouveau paradigme de État a émergé : État asynchrone.

État local

Très bien, tous ceux qui ont déjà travaillé avec React savent ce qu'est un État local.

Je ne sais pas ce que c'est
L'État local est l'état d'un seul composant.

Chaque fois qu'un état est mis à jour, le composant est à nouveau rendu.


Vous avez peut-être travaillé avec cette ancienne structure :

class CommitList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isLoading: false,
      commits: [],
      error: null
    };
  }

  componentDidMount() {
    this.fetchCommits();
  }

  fetchCommits = async () => {
    this.setState({ isLoading: true });
    try {
      const response = await fetch('https://api.github.com/repos/facebook/react/commits');
      const data = await response.json();
      this.setState({ commits: data, isLoading: false });
    } catch (error) {
      this.setState({ error: error.message, isLoading: false });
    }
  };

  render() {
    const { isLoading, commits, error } = this.state;

    if (isLoading) return <div>Loading...</div>;
    if (error) return <div>Error: {error}</div>;

    return (
      <div>
        <h2>Commit List</h2>
        <ul>
          {commits.map(commit => (
            <li key={commit.sha}>{commit.commit.message}</li>
          ))}
        </ul>
        <TotalCommitsCount count={commits.length} />
      </div>
    );
  }
}

class TotalCommitsCount extends Component {
  render() {
    return <div>Total commits: {this.props.count}</div>;
  }
}
}

Peut-être un moderne fonctionnel :

const CommitList = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [commits, setCommits] = useState([]);
  const [error, setError] = useState(null);

  // To update state you can use setIsLoading, setCommits or setUsername.
  // As each function will overwrite only the state bound to it.
  // NOTE: It will still cause a full-component re-render
  useEffect(() => {
    const fetchCommits = async () => {
      setIsLoading(true);
      try {
        const response = await fetch('https://api.github.com/repos/facebook/react/commits');
        const data = await response.json();
        setCommits(data);
        setIsLoading(false);
      } catch (error) {
        setError(error.message);
        setIsLoading(false);
      }
    };

    fetchCommits();
  }, []);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <h2>Commit List</h2>
      <ul>
        {commits.map(commit => (
          <li key={commit.sha}>{commit.commit.message}</li>
        ))}
      </ul>
      <TotalCommitsCount count={commits.length} />
    </div>
  );
};

const TotalCommitsCount = ({ count }) => {
  return <div>Total commits: {count}</div>;
};

Ou même un "plus accepté" ? (Certainement plus rare cependant)

const initialState = {
  isLoading: false,
  commits: [],
  userName: ''
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'SET_LOADING':
      return { ...state, isLoading: action.payload };
    case 'SET_COMMITS':
      return { ...state, commits: action.payload };
    case 'SET_USERNAME':
      return { ...state, userName: action.payload };
    default:
      return state;
  }
};

const CommitList = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { isLoading, commits, userName } = state;

  // To update state, use dispatch. For example:
  // dispatch({ type: 'SET_LOADING', payload: true });
  // dispatch({ type: 'SET_COMMITS', payload: [...] });
  // dispatch({ type: 'SET_USERNAME', payload: 'newUsername' });
};

Ce qui peut faire réfléchir...

Pourquoi hack j'écrirais ce réducteur complexe pour un seul composant ?

Eh bien, React a hérité de ce crochet moche appelé useReducer d'un outil très important appelé Redux.

Si vous avez déjà eu affaire à la Gestion globale de l'état dans React, vous devez avoir entendu parler de Redux.

Cela nous amène au sujet suivant : la gestion globale de l'état.

État global

Global State Management est l'une des premières matières complexes lors de l'apprentissage de React.

Qu'est-ce que c'est?

Il peut s'agir de plusieurs choses, construites de plusieurs manières, avec différentes bibliothèques.

J'aime le définir comme :

Un seul objet JSON, accessible et maintenu par n'importe quel composant de l'application.

const globalState = { 
  isUnique: true,
  isAccessible: true,
  isModifiable: true,
  isFEOnly: true
}

J'aime y penser comme :

Une base de données frontale No-SQL.

C'est vrai, une base de données. C'est là que vous stockez les données d'application, que vos composants peuvent lire/écrire/mettre à jour/supprimer.

Je sais que, par défaut, l'état sera recréé chaque fois que l'utilisateur rechargera la page, mais ce n'est peut-être pas ce que vous voulez qu'il fasse, et si vous conservez des données quelque part (comme le localStorage), vous voudrez peut-être pour en savoir plus sur les migrations pour éviter de casser l'application à chaque nouveau déploiement.

J'aime l'utiliser comme :

Un portail multidimensionnel, où les composants peuvent envoyer leurs sentiments et sélectionner leurs attributs. Tout, partout, tout à la fois.

Comment l'utiliser ?

La voie principale

Redux

C'est la norme de l'industrie.

Je travaille avec React, TypeScript et Redux depuis 7 ans. Chaque projet avec lequel j'ai travaillé professionnellement utilise Redux.

La grande majorité des personnes que j'ai rencontrées et qui travaillent avec React utilisent Redux.

L'outil le plus mentionné dans les postes vacants React chez Trampar de Casa est Redux.

L'outil de gestion d'état React le plus populaire est...

The Evolution of React State Management: From Local to Async

Redux

The Evolution of React State Management: From Local to Async

Si vous souhaitez travailler avec React, vous devriez apprendre Redux.
Si vous travaillez actuellement avec React, vous le savez probablement déjà.

Ok, voici comment nous récupérons habituellement les données en utilisant Redux.

Avis de non-responsabilité
"Quoi ? Est-ce que cela a du sens ? Redux consiste à stocker des données, pas à les récupérer, comment récupérez-vous des données avec Redux ?"

Si vous y avez pensé, je dois vous le dire :

Je ne récupère réellement des données avec Redux.
Redux sera le cabinet de l'application, il stockera les états ~shoes~ qui sont directement liés à la récupération, c'est pourquoi j'ai utilisé cette mauvaise expression : "récupérer des données en utilisant Redux".


// actions
export const SET_LOADING = 'SET_LOADING';
export const setLoading = (isLoading) => ({
  type: SET_LOADING,
  payload: isLoading,
});

export const SET_ERROR = 'SET_ERROR';
export const setError = (isError) => ({
  type: SET_ERROR,
  payload: isError,
});

export const SET_COMMITS = 'SET_COMMITS';
export const setCommits = (commits) => ({
  type: SET_COMMITS,
  payload: commits,
});


// To be able to use ASYNC action, it's required to use redux-thunk as a middleware
export const fetchCommits = () => async (dispatch) => {
  dispatch(setLoading(true));
  try {
    const response = await fetch('https://api.github.com/repos/facebook/react/commits');
    const data = await response.json();
    dispatch(setCommits(data));
    dispatch(setError(false));
  } catch (error) {
    dispatch(setError(true));
  } finally {
    dispatch(setLoading(false));
  }
};

// the state shared between 2-to-many components
const initialState = {
  isLoading: false,
  isError: false,
  commits: [],
};

// reducer
export const rootReducer = (state = initialState, action) => {
  // This could also be actions[action.type].
  switch (action.type) {
    case SET_LOADING:
      return { ...state, isLoading: action.payload };
    case SET_ERROR:
      return { ...state, isError: action.payload };
    case SET_COMMITS:
      return { ...state, commits: action.payload };
    default:
      return state;
  }
};

Maintenant côté interface utilisateur, nous intégrons des actions en utilisant useDispatch et useSelector :

// Commits.tsx
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchCommits } from './action';

export const Commits = () => {
  const dispatch = useDispatch();
  const { isLoading, isError, commits } = useSelector(state => state);

  useEffect(() => {
    dispatch(fetchCommits());
  }, [dispatch]);

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error while trying to fetch commits.</div>;

  return (
    <ul>
      {commits.map(commit => (
        <li key={commit.sha}>{commit.commit.message}</li>
      ))}
    </ul>
  );
};

Si Commits.tsx était le seul composant qui devait accéder à la liste des commits, vous ne devriez pas stocker ces données sur l'état global. Il pourrait utiliser l'État local à la place.

But let's suppose you have other components that need to interact with this list, one of them may be as simple as this one:

// TotalCommitsCount.tsx
import React from 'react';
import { useSelector } from 'react-redux';

export const TotalCommitsCount = () => {
  const commitCount = useSelector(state => state.commits.length);
  return <div>Total commits: {commitCount}</div>;
}

Disclaimer
In theory, this piece of code would make more sense living inside Commits.tsx, but let's assume we want to display this component in multiple places of the app and it makes sense to put the commits list on the Global State and to have this TotalCommitsCount component.

With the index.js component being something like this:

import React from 'react';
import ReactDOM from 'react-dom';
import thunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { Commits } from "./Commits"
import { TotalCommitsCount } from "./TotalCommitsCount"

export const App = () => (
    <main>
        <TotalCommitsCount />
        <Commits />
    </main>
)

const store = createStore(rootReducer, applyMiddleware(thunk));
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

This works, but man, that looks overly complicated for something as simple as fetching data right?

Redux feels a little too bloated to me.

You're forced to create actions and reducers, often also need to create a string name for the action to be used inside the reducer, and depending on the folder structure of the project, each layer could be in a different file.

Which is not productive.

But wait, there is a simpler way.

The simple way

Zustand

At the time I'm writing this article, Zustand has 3,495,826 million weekly downloads, more than 45,000 stars on GitHub, and 2, that's right, TWO open Pull Requests.

ONE OF THEM IS ABOUT UPDATING IT'S DOC
The Evolution of React State Management: From Local to Async

If this is not a piece of Software Programming art, I don't know what it is.

Here's how to replicate the previous code using Zustand.

// store.js
import create from 'zustand';

const useStore = create((set) => ({
  isLoading: false,
  isError: false,
  commits: [],
  fetchCommits: async () => {
    set({ isLoading: true });
    try {
      const response = await fetch('https://api.github.com/repos/facebook/react/commits');
      const data = await response.json();
      set({ commits: data, isError: false });
    } catch (error) {
      set({ isError: true });
    } finally {
      set({ isLoading: false });
    }
  },
}));

This was our Store, now the UI.

// Commits.tsx
import React, { useEffect } from 'react';
import useStore from './store';

export const Commits = () => {
  const { isLoading, isError, commits, fetchCommits } = useStore();

  useEffect(() => {
    fetchCommits();
  }, [fetchCommits]);

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error occurred</div>;

  return (
    <ul>
      {commits.map(commit => (
        <li key={commit.sha}>{commit.commit.message}</li>
      ))}
    </ul>
  );
}

And last but not least.

// TotalCommitsCount.tsx
import React from 'react'; 
import useStore from './store'; 
const TotalCommitsCount = () => { 
    const totalCommits = useStore(state => state.commits.length);
    return ( 
        <div> 
            <h2>Total Commits:</h2> <p>{totalCommits}</p> 
        </div> 
    ); 
};

There are no actions and reducers, there is a Store.

And it's advisable to have slices of Store, so everything is near to the feature related to the data.

It works perfect with a folder-by-feature folder structure.
The Evolution of React State Management: From Local to Async

The wrong way

I need to confess something, both of my previous examples are wrong.

And let me do a quick disclaimer: They're not wrong, they're outdated, and therefore, wrong.

This wasn't always wrong though. That's how we used to develop data fetching in React applications a while ago, and you may still find code similar to this one out there in the world.

But there is another way.

An easier one, and more aligned with an essential feature for web development: Caching. But I'll get back to this subject later.

Currently, to fetch data in a single component, the following flow is required:
The Evolution of React State Management: From Local to Async

What happens if I need to fetch data from 20 endpoints inside 20 components?

  • 20x isLoading + 20x isError + 20x actions to mutate this properties.

What will they look like?

With 20 endpoints, this will become a very repetitive process and will cause a good amount of duplicated code.

What if you need to implement a caching feature to prevent recalling the same endpoint in a short period? (or any other condition)

Well, that will translate into a lot of work for basic features (like caching) and well-written components that are prepared for loading/error states.

This is why Async State was born.

Async State

Before talking about Async State I want to mention something. We know how to use Local and Global state but at this time I didn't mention what should be stored and why.

The Global State example has a flaw and an important one.

The TotalCommitsCount component will always display the Commits Count, even if it's loading or has an error.

If the request failed, there's no way to know that the Total Commits Count is 0, so presenting this value is presenting a lie.

In fact, until the request finishes, there is no way to know for sure what's the Total Commits Count value.

This is because the Total Commits Count is not a value we have inside the application. It's external information, async stuff, you know.

We shouldn't be telling lies if we don't know the truth.

That's why we must identify Async State in our application and create components prepared for it.

We can do this with React-Query, SWR, Redux Toolkit Query and many others.

The Evolution of React State Management: From Local to Async

Pour cet article, j'utiliserai React-Query.

Je vous recommande d'accéder à la documentation de chacun de ces outils pour mieux comprendre quels problèmes ils résolvent.

Voici le code :

Plus d'actions, plus d'envois, plus de État global

pour récupérer des données.

Voici ce que vous devez faire dans votre fichier App.tsx pour que React-Query soit correctement configuré :

Vous voyez, Async State

est spécial.

C'est comme le chat de Schrödinger : vous ne connaissez pas l'état tant que vous ne l'avez pas observé (ou exécuté).

Mais attendez, si les deux composants appellent useCommits et que useCommits appelle un point de terminaison d'API, cela signifie-t-il qu'il y aura DEUX requêtes identiques pour charger les mêmes données ?

Réponse courte : non !

Réponse longue : React Query est génial. Il gère automatiquement cette situation pour vous, il est livré avec une mise en cache préconfigurée qui est suffisamment intelligente pour savoir quand récupérer

vos données ou simplement utiliser le cache.

Il est également extrêmement configurable afin que vous puissiez l'ajuster pour répondre à 100 % des besoins de votre application.

Maintenant, nos composants sont toujours prêts pour isLoading ou isError et nous gardons l'état global moins pollué et avons des fonctionnalités assez intéressantes prêtes à l'emploi.

Conclusion

Vous connaissez maintenant la différence entre Local, Global et Async State

.


Local -> Composant uniquement.
Mondial -> Single-Json-NoSQL-DB-For-The-FE.

Asynchrone -> Données externes, semblables à un chat de Schrödinger, vivant en dehors de l'application FE qui nécessitent un chargement et peuvent renvoyer une erreur.

J'espère que cet article vous a plu, n'hésitez pas à me faire savoir si vous avez des avis différents ou des retours constructifs, bravo !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn