在页面刷新之前,React 上下文不会更新状态
React context does not update state until the page is refreshed
我有一份患者名单。我需要显示单个患者的数据。使用 id 唯一标识患者。当我使用 axios 从后端获取特定患者的数据时,它似乎工作得很好。但问题是,当我点击患者的 link 时,我需要状态更新到那个对象。
当我从 reducer.ts 文件登录时,负载完美加载。但是,当我点击 link 实际显示数据时,直到刷新页面才显示。但是,刷新后它确实将状态更改为我想要的唯一对象,然后恢复到初始状态。我希望它获得对象并将其保持在状态中。我哪里错了?
页面刷新前:
页面刷新后:
index.ts:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { reducer, StateProvider } from "./state";
ReactDOM.render(
<StateProvider reducer={reducer}>
<App />
</StateProvider>,
document.getElementById('root')
);
Reducer.ts:
import { State } from "./state";
import { Patient } from "../types";
export type Action =
| {
type: "SET_PATIENT_LIST";
payload: Patient[];
}
| {
type: "SINGLE_PATIENT";
payload: Patient;
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "SET_PATIENT_LIST":
return {
...state,
patients: {
...action.payload.reduce(
(memo, patient) => ({ ...memo, [patient.id]: patient }),
{}
),
...state.patients
}
};
case "SINGLE_PATIENT":
console.log(action.payload);
return {
...state,
patients: {
...state.patients,
[action.payload.id]: {
...state.patients[action.payload.id],
...action.payload,
},
}
};
default:
return state;
}
};
export const setPatientList = (patientList: Patient[]): Action => {
return {
type: "SET_PATIENT_LIST",
payload: patientList
};
};
export const setSinglePatient = (patient: Patient): Action => {
return {
type: "SINGLE_PATIENT",
payload: patient
};
};
State.tsx:
import React, { createContext, useContext, useReducer } from "react";
import { Patient } from "../types";
import { Action } from "./reducer";
export type State = {
patients: { [id: string]: Patient };
};
const initialState: State = {
patients: {}
};
export const StateContext = createContext<[State, React.Dispatch<Action>]>([initialState, () => initialState]);
type StateProviderProps = {
reducer: React.Reducer<State, Action>;
children: React.ReactElement;
};
export const StateProvider = ({
reducer,
children
}: StateProviderProps) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={[state, dispatch]}>
{children}
</StateContext.Provider>
);
};
export const useStateValue = () => useContext(StateContext);
App.tsx:
import React from "react";
import axios from "axios";
import { BrowserRouter as Router, Route, Link, Routes } from "react-router-dom";
import { Button, Divider, Container, Typography } from "@material-ui/core";
import { apiBaseUrl } from "./constants";
import { useStateValue, setPatientList } from "./state";
import { Patient } from "./types";
import PatientListPage from "./PatientListPage";
import SinglePatientPage from "./SinglePatientPage";
const App = () => {
const [, dispatch] = useStateValue();
React.useEffect(() => {
void axios.get<void>(`${apiBaseUrl}/ping`);
const fetchPatientList = async () => {
try {
const { data: patientListFromApi } = await axios.get<Patient[]>(
`${apiBaseUrl}/patients`
);
dispatch(setPatientList(patientListFromApi));
} catch (e) {
console.error(e);
}
};
void fetchPatientList();
}, [dispatch]);
return (
<div className="App">
<Router>
<Container>
<Typography variant="h3" style={{ marginBottom: "0.5em" }}>
Patientor
</Typography>
<Button component={Link} to="/" variant="contained" color="primary">
Home
</Button>
<Divider hidden />
<Routes>
<Route path="/" element={<PatientListPage />} />
<Route path="/patients/:id" element={<SinglePatientPage />} />
</Routes>
</Container>
</Router>
</div>
);
};
export default App;
PatientListPage.tsx:
import React from "react";
import axios from "axios";
import {
Box,
Table,
Button,
TableHead,
Typography,
TableCell,
TableRow,
TableBody
} from "@material-ui/core";
import { PatientFormValues } from "../AddPatientModal/AddPatientForm";
import AddPatientModal from "../AddPatientModal";
import { Patient } from "../types";
import { apiBaseUrl } from "../constants";
import HealthRatingBar from "../components/HealthRatingBar";
import { useStateValue } from "../state";
import { Link } from 'react-router-dom';
const PatientListPage = () => {
const [{ patients }, dispatch] = useStateValue();
const [error, setError] = React.useState<string>();
return (
<div className="App">
<Box>
<Typography align="center" variant="h6">
Patient list
</Typography>
</Box>
<Table style={{ marginBottom: "1em" }}>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Gender</TableCell>
<TableCell>Occupation</TableCell>
<TableCell>Health Rating</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.values(patients).map((patient: Patient) => (
<TableRow key={patient.id}>
<TableCell>
<Link to={`/patients/${patient.id}`}>
{patient.name}
</Link>
</TableCell>
<TableCell>{patient.gender}</TableCell>
<TableCell>{patient.occupation}</TableCell>
<TableCell>
<HealthRatingBar showText={false} rating={1} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
export default PatientListPage;
单个患者页面:
import React from "react";
import { Patient } from "../types";
import { useStateValue, setSinglePatient } from "../state";
import { useParams } from "react-router-dom";
import { Typography } from "@material-ui/core";
import { apiBaseUrl } from "../constants";
import axios from "axios";
const SinglePatientPage = () => {
const [{ patients }, dispatch] = useStateValue();
const { id } = useParams<{ id: string }>();
React.useEffect(() => {
const fetchSinglePatient = async () => {
if(id !== undefined) {
try {
const { data: patientFromApi } = await axios.get<Patient>(
`${apiBaseUrl}/patients/${id}`
);
dispatch(setSinglePatient(patientFromApi));
} catch (e) {
console.error(e);
}
}
};
void fetchSinglePatient();
}, [dispatch]);
if (patients) {
console.log('inside singlepatientpage', patients);
return (
<div className="app">
<Typography variant="h6" style={{ marginBottom: "0.5em" }}>
{patient.name}
<p>ssn: {patient.ssn}</p>
<p>occupation: {patient.occupation}</p>
</Typography>
</div>
);
}
return null;
};
export default SinglePatientPage;
似乎缺少 SinglePatientPage
组件,使用 id
路由参数作为获取特定患者记录的依赖项。
将 id
添加到 useEffect
挂钩的依赖数组,因为它在回调中被引用。
const SinglePatientPage = () => {
const [{ patients }, dispatch] = useStateValue();
const { id } = useParams<{ id: string }>();
React.useEffect(() => {
const fetchSinglePatient = async () => {
if(id !== undefined) {
try {
const { data: patientFromApi } = await axios.get<Patient>(
`${apiBaseUrl}/patients/${id}` // <-- id referenced here
);
dispatch(setSinglePatient(patientFromApi));
} catch (e) {
console.error(e);
}
}
};
void fetchSinglePatient();
}, [dispatch, id]); // <-- add id dependency
if (!patients) {
return null;
}
return (
<div className="app">
<Typography variant="h6" style={{ marginBottom: "0.5em" }}>
{patient.name}
<p>ssn: {patient.ssn}</p>
<p>occupation: {patient.occupation}</p>
</Typography>
</div>
);
};
您可能会考虑将 React hooks eslint rules 添加到您的项目中,以帮助在将来捕获此类缺失的依赖项。
我知道问题出在哪里了。我没有为单个患者数据创建新状态。
export type State = {
patients: { [id: string]: Patient };
patient: Patient | null; //new state
};
const initialState: State = {
patients: {},
patient: null //new initial state
};
在减速器中,我正在 return 处理一组患者。现在我可以 return 单个患者的数据。
case "SINGLE_PATIENT":
return {
...state,
patient: action.payload // returning "patient" instead of "patients"
};
我有一份患者名单。我需要显示单个患者的数据。使用 id 唯一标识患者。当我使用 axios 从后端获取特定患者的数据时,它似乎工作得很好。但问题是,当我点击患者的 link 时,我需要状态更新到那个对象。
当我从 reducer.ts 文件登录时,负载完美加载。但是,当我点击 link 实际显示数据时,直到刷新页面才显示。但是,刷新后它确实将状态更改为我想要的唯一对象,然后恢复到初始状态。我希望它获得对象并将其保持在状态中。我哪里错了?
页面刷新前:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { reducer, StateProvider } from "./state";
ReactDOM.render(
<StateProvider reducer={reducer}>
<App />
</StateProvider>,
document.getElementById('root')
);
Reducer.ts:
import { State } from "./state";
import { Patient } from "../types";
export type Action =
| {
type: "SET_PATIENT_LIST";
payload: Patient[];
}
| {
type: "SINGLE_PATIENT";
payload: Patient;
};
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "SET_PATIENT_LIST":
return {
...state,
patients: {
...action.payload.reduce(
(memo, patient) => ({ ...memo, [patient.id]: patient }),
{}
),
...state.patients
}
};
case "SINGLE_PATIENT":
console.log(action.payload);
return {
...state,
patients: {
...state.patients,
[action.payload.id]: {
...state.patients[action.payload.id],
...action.payload,
},
}
};
default:
return state;
}
};
export const setPatientList = (patientList: Patient[]): Action => {
return {
type: "SET_PATIENT_LIST",
payload: patientList
};
};
export const setSinglePatient = (patient: Patient): Action => {
return {
type: "SINGLE_PATIENT",
payload: patient
};
};
State.tsx:
import React, { createContext, useContext, useReducer } from "react";
import { Patient } from "../types";
import { Action } from "./reducer";
export type State = {
patients: { [id: string]: Patient };
};
const initialState: State = {
patients: {}
};
export const StateContext = createContext<[State, React.Dispatch<Action>]>([initialState, () => initialState]);
type StateProviderProps = {
reducer: React.Reducer<State, Action>;
children: React.ReactElement;
};
export const StateProvider = ({
reducer,
children
}: StateProviderProps) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={[state, dispatch]}>
{children}
</StateContext.Provider>
);
};
export const useStateValue = () => useContext(StateContext);
App.tsx:
import React from "react";
import axios from "axios";
import { BrowserRouter as Router, Route, Link, Routes } from "react-router-dom";
import { Button, Divider, Container, Typography } from "@material-ui/core";
import { apiBaseUrl } from "./constants";
import { useStateValue, setPatientList } from "./state";
import { Patient } from "./types";
import PatientListPage from "./PatientListPage";
import SinglePatientPage from "./SinglePatientPage";
const App = () => {
const [, dispatch] = useStateValue();
React.useEffect(() => {
void axios.get<void>(`${apiBaseUrl}/ping`);
const fetchPatientList = async () => {
try {
const { data: patientListFromApi } = await axios.get<Patient[]>(
`${apiBaseUrl}/patients`
);
dispatch(setPatientList(patientListFromApi));
} catch (e) {
console.error(e);
}
};
void fetchPatientList();
}, [dispatch]);
return (
<div className="App">
<Router>
<Container>
<Typography variant="h3" style={{ marginBottom: "0.5em" }}>
Patientor
</Typography>
<Button component={Link} to="/" variant="contained" color="primary">
Home
</Button>
<Divider hidden />
<Routes>
<Route path="/" element={<PatientListPage />} />
<Route path="/patients/:id" element={<SinglePatientPage />} />
</Routes>
</Container>
</Router>
</div>
);
};
export default App;
PatientListPage.tsx:
import React from "react";
import axios from "axios";
import {
Box,
Table,
Button,
TableHead,
Typography,
TableCell,
TableRow,
TableBody
} from "@material-ui/core";
import { PatientFormValues } from "../AddPatientModal/AddPatientForm";
import AddPatientModal from "../AddPatientModal";
import { Patient } from "../types";
import { apiBaseUrl } from "../constants";
import HealthRatingBar from "../components/HealthRatingBar";
import { useStateValue } from "../state";
import { Link } from 'react-router-dom';
const PatientListPage = () => {
const [{ patients }, dispatch] = useStateValue();
const [error, setError] = React.useState<string>();
return (
<div className="App">
<Box>
<Typography align="center" variant="h6">
Patient list
</Typography>
</Box>
<Table style={{ marginBottom: "1em" }}>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Gender</TableCell>
<TableCell>Occupation</TableCell>
<TableCell>Health Rating</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.values(patients).map((patient: Patient) => (
<TableRow key={patient.id}>
<TableCell>
<Link to={`/patients/${patient.id}`}>
{patient.name}
</Link>
</TableCell>
<TableCell>{patient.gender}</TableCell>
<TableCell>{patient.occupation}</TableCell>
<TableCell>
<HealthRatingBar showText={false} rating={1} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
export default PatientListPage;
单个患者页面:
import React from "react";
import { Patient } from "../types";
import { useStateValue, setSinglePatient } from "../state";
import { useParams } from "react-router-dom";
import { Typography } from "@material-ui/core";
import { apiBaseUrl } from "../constants";
import axios from "axios";
const SinglePatientPage = () => {
const [{ patients }, dispatch] = useStateValue();
const { id } = useParams<{ id: string }>();
React.useEffect(() => {
const fetchSinglePatient = async () => {
if(id !== undefined) {
try {
const { data: patientFromApi } = await axios.get<Patient>(
`${apiBaseUrl}/patients/${id}`
);
dispatch(setSinglePatient(patientFromApi));
} catch (e) {
console.error(e);
}
}
};
void fetchSinglePatient();
}, [dispatch]);
if (patients) {
console.log('inside singlepatientpage', patients);
return (
<div className="app">
<Typography variant="h6" style={{ marginBottom: "0.5em" }}>
{patient.name}
<p>ssn: {patient.ssn}</p>
<p>occupation: {patient.occupation}</p>
</Typography>
</div>
);
}
return null;
};
export default SinglePatientPage;
似乎缺少 SinglePatientPage
组件,使用 id
路由参数作为获取特定患者记录的依赖项。
将 id
添加到 useEffect
挂钩的依赖数组,因为它在回调中被引用。
const SinglePatientPage = () => {
const [{ patients }, dispatch] = useStateValue();
const { id } = useParams<{ id: string }>();
React.useEffect(() => {
const fetchSinglePatient = async () => {
if(id !== undefined) {
try {
const { data: patientFromApi } = await axios.get<Patient>(
`${apiBaseUrl}/patients/${id}` // <-- id referenced here
);
dispatch(setSinglePatient(patientFromApi));
} catch (e) {
console.error(e);
}
}
};
void fetchSinglePatient();
}, [dispatch, id]); // <-- add id dependency
if (!patients) {
return null;
}
return (
<div className="app">
<Typography variant="h6" style={{ marginBottom: "0.5em" }}>
{patient.name}
<p>ssn: {patient.ssn}</p>
<p>occupation: {patient.occupation}</p>
</Typography>
</div>
);
};
您可能会考虑将 React hooks eslint rules 添加到您的项目中,以帮助在将来捕获此类缺失的依赖项。
我知道问题出在哪里了。我没有为单个患者数据创建新状态。
export type State = {
patients: { [id: string]: Patient };
patient: Patient | null; //new state
};
const initialState: State = {
patients: {},
patient: null //new initial state
};
在减速器中,我正在 return 处理一组患者。现在我可以 return 单个患者的数据。
case "SINGLE_PATIENT":
return {
...state,
patient: action.payload // returning "patient" instead of "patients"
};