Hai!
Artikel ini membentangkan gambaran keseluruhan tentang cara State diuruskan dalam React Applications beribu-ribu tahun yang lalu apabila Komponen Kelas menguasai dunia dan komponen berfungsi hanyalah idea yang berani, sehingga kebelakangan ini , apabila paradigma baharu State telah muncul: Async State.
Baiklah, semua orang yang telah bekerja dengan React tahu apa itu Negeri Tempatan.
Setiap kali keadaan dikemas kini, komponen itu dipaparkan semula.Saya tidak tahu apa itu
Negeri Tempatan ialah keadaan bagi satu Komponen.
Anda mungkin pernah bekerja dengan struktur purba ini:
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>; } } }
Mungkin moden berfungsi satu:
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>; };
Atau malah "lebih diterima" satu? (Semestinya lebih jarang berlaku)
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' }); };
Yang boleh membuatkan anda tertanya-tanya...
Mengapa godam saya akan menulis pengurang kompleks ini untuk satu komponen?
Nah, React mewarisi cangkuk hodoh ini yang dipanggil useReducer daripada alat yang sangat penting yang dipanggil Redux.
Jika anda pernah berhadapan dengan Pengurusan Negara Global dalam React, anda mesti pernah mendengar tentang Redux.
Ini membawa kita ke topik seterusnya: Pengurusan Negeri Global.
Pengurusan Negara Global ialah salah satu mata pelajaran kompleks pertama apabila mempelajari React.
Ia boleh menjadi berbilang perkara, dibina dalam pelbagai cara, dengan perpustakaan yang berbeza.
Saya suka mentakrifkannya sebagai:
Satu objek JSON, diakses dan diselenggara oleh mana-mana Komponen aplikasi.
const globalState = { isUnique: true, isAccessible: true, isModifiable: true, isFEOnly: true }
Saya suka menganggapnya sebagai:
Pangkalan Data Hadapan Tanpa SQL.
Betul, Pangkalan Data. Di sinilah anda menyimpan data aplikasi, yang komponen anda boleh membaca/menulis/kemas kini/padam.
Saya tahu, secara lalai, keadaan akan dicipta semula apabila pengguna memuat semula halaman, tetapi itu mungkin bukan perkara yang anda mahukannya lakukan, dan jika anda meneruskan data di suatu tempat (seperti LocalStorage), anda mungkin mahu untuk mengetahui tentang penghijrahan untuk mengelak daripada memecahkan apl setiap penggunaan baharu.
Saya suka menggunakannya sebagai:
Portal berbilang dimensi, di mana komponen boleh menghantar perasaan mereka dan memilih atribut mereka. Semuanya, di mana-mana, sekali gus.
Redux
Ia adalah standard industri.
Saya telah bekerja dengan React, TypeScript dan Redux selama 7 tahun. Setiap projek yang saya telah bekerjasama secara profesional menggunakan Redux.
Sebilangan besar orang yang saya temui yang bekerja dengan React, gunakan Redux.
Alat yang paling banyak disebut dalam kedudukan terbuka React di Trampar de Casa ialah Redux.
Alat React State Management yang paling popular ialah...
Redux
Jika anda ingin bekerja dengan React, anda harus belajar Redux.
Jika anda kini bekerja dengan React, anda mungkin sudah tahu.
Ok, berikut ialah cara kami biasanya mengambil data menggunakan Redux.
Jika anda memikirkan perkara ini, saya mesti memberitahu anda: Saya sebenarnya tidak mengambil data dengan Redux. Penafian
"Apa? Adakah ini masuk akal? Redux adalah untuk menyimpan data, bukan untuk mengambil, bagaimana F anda akan mengambil data dengan Redux?"
Redux akan menjadi kabinet untuk aplikasi, ia akan menyimpan ~kasut~ menyatakan yang berkaitan secara langsung dengan mengambil, itulah sebabnya saya menggunakan frasa yang salah ini: "ambil data menggunakan 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; } };
Kini di bahagian UI, kami menyepadukan dengan tindakan menggunakan useDispatch dan 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> ); };
Jika Commits.tsx ialah satu-satunya komponen yang diperlukan untuk mengakses senarai komit, anda tidak seharusnya menyimpan data ini pada Keadaan Global. Ia boleh menggunakan negeri tempatan sebaliknya.
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.
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
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.
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:
What happens if I need to fetch data from 20 endpoints inside 20 components?
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.
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.
Untuk artikel ini, saya akan menggunakan React-Query.
Saya mengesyorkan anda untuk mengakses dokumen setiap alat ini untuk memahami dengan lebih baik masalah yang mereka selesaikan.
Ini kodnya:
Tiada lagi tindakan, tiada lagi penghantaran, tiada lagi Negeri Global
untuk mengambil data.Inilah yang anda perlu lakukan dalam fail App.tsx anda untuk mempunyai React-Query dikonfigurasikan dengan betul:
Anda lihat, Async State
adalah istimewa.Ia seperti kucing Schrödinger – anda tidak tahu keadaannya sehingga anda memerhatikannya (atau menjalankannya).
Tetapi tunggu, jika kedua-dua komponen memanggil useCommits dan useCommits memanggil titik akhir API, adakah ini bermakna terdapat DUA permintaan yang sama untuk memuatkan data yang sama?
Jawapan Ringkas: tidak!
Jawapan Panjang: React Query adalah hebat. Ia secara automatik mengendalikan situasi ini untuk anda, ia disertakan dengan caching prakonfigurasi yang cukup pintar untuk mengetahui bila mendapatkan semula
data anda atau hanya menggunakan cache.Ia juga sangat boleh dikonfigurasikan supaya anda boleh mengubah suai agar muat 100% daripada keperluan aplikasi anda.
Kini kami mempunyai komponen kami sentiasa bersedia untuk isLoading atau isError dan kami memastikan Keadaan Global kurang tercemar dan mempunyai beberapa ciri yang cukup kemas di luar kotak.
Sekarang anda tahu perbezaan antara Tempatan, Global dan Async State
.
Tempatan -> Komponen Sahaja.
Global -> Single-Json-NoSQL-DB-For-The-FE.
Saya harap anda menikmati artikel ini, beritahu saya jika anda mempunyai pendapat yang berbeza atau sebarang maklum balas yang membina, sorakan!
Atas ialah kandungan terperinci Evolusi Pengurusan Keadaan React: Dari Tempatan kepada Async. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!