I have a list of patients. I need to display the data of a single patient. The patients are identified uniquely using an id. When I get a particular patient's data from the backend using axios, it seems to work perfectly. But the problem is that I need the state to update to that object when I click on the patient's link. When I log from the reducer.ts file, the payload loads perfectly. But then, when I click on the link to actually show the data, it does not show it until the page is refreshed. However, after it is refreshed it does change the state to the unique object I want, and then reverts back to the initial state. I want it to get the object and keep it in the state. Where am I going wrong?
Before the page is refreshed:
After the page is refreshed:
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;
SinglePatientPage:
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;
It appears the SinglePatientPage component is missing using the id route param as a dependency for fetching the specific patient record.
Add id to the useEffect hook's dependency array since it is referenced in the callback.
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>
);
};
You might consider adding the React hooks eslint rules to your project to help catch missing dependencies like this in the future.
I figured out what the problem was. I hadn't made a new state for singular patient data.
export type State = {
patients: { [id: string]: Patient };
patient: Patient | null; //new state
};
const initialState: State = {
patients: {},
patient: null //new initial state
};
And in the reducer, I was returning an array of patients. Now I can return a single patient's data.
case "SINGLE_PATIENT":
return {
...state,
patient: action.payload // returning "patient" instead of "patients"
};
Related
I think the Space.js and Loading.js components re-render far too many times. I can understand it up til the very last 4 re-renders. What is causing them is a mystery.
[slug].js
import { useRouter } from "next/router";
import AuthLayout from "#/components/layouts/AuthLayout";
import { useEffect, useState } from "react";
import axios from "#/src/lib/axios";
import Container from "#mui/material/Container";
import Typography from "#mui/material/Typography";
import useBearStore from "stores/pages";
import Loading from "#/components/layouts/Auth/Loading";
export default function Space() {
console.log('Space.js: function()');
const {setLoading} = useBearStore();
const router = useRouter();
const { slug } = router.query;
const [space, setSpace] = useState(null);
const getSpace = async () => {
const { data } = await axios.get(`api/spaces/${slug}`);
setSpace(data.data);
setLoading(false);
};
useEffect(() => {
console.log("Space.js: useEffect()");
getSpace();
}, [slug]);
return (
<Loading>
<Container maxWidth="sm">
<Typography variant="h1" component="h1" gutterBottom>
{space?.name}
</Typography>
</Container>
</Loading>
);
}
Space.getLayout = function getLayout(page) {
return <AuthLayout>{page}</AuthLayout>;
};
Loading.js
import useBearStore from "stores/pages";
import { LinearProgress } from "#mui/material";
import { useEffect } from 'react'
export default function Loading({children}) {
const {loading, setLoading} = useBearStore();
console.log('Loading.js: function()');
useEffect(() => {
console.log('Loading.js: useEffect()');
setLoading(true);
}, []);
return loading ? <LinearProgress /> : children;
}
pages.js
import create from 'zustand'
const useBearStore = create((set) => ({
loading: true,
setLoading: (value) => set((state) => ({ loading: value }))
}))
export default useBearStore;
next.config.js
module.exports = {
// reactStrictMode: true,
// images: {
// domains: ["images.pexels.com"],
// },
};
How to link react with redux to add and remove selected movies to the array by clicking on the heart to display them on the favoŠ³ites page
Bellow my redux:
action:
export function addMovie(id) {
return {
type: 'ADD_MOVIE',
payload: id
}
}
export function removeMovie(id) {
return {
type: 'REMOVE_MOVIE',
payload: id
}
}
reducer:
function favouriteReducer(state = [], action) {
switch(action.type) {
case 'ADD_MOVIE':
return {
items: state.items.concat(action.payload)
}
case 'REMOVE_MOVIE':
return {
items: state.items.filter(item => item !== action.payload)
}
default:
return state;
}
}
export default favouriteReducer;
store:
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import favouriteReducer from "./reducers/favouriteReducer";
const redusers = combineReducers({
favourite: favouriteReducer,
});
const store = createStore(redusers, composeWithDevTools(applyMiddleware(thunk)));
export default store;
and my component, where i need to click to add or remove chosen movie
import React, { useEffect, useState } from "react";
import './index.css';
import { Link, useParams } from "react-router-dom";
import { connect } from 'react-redux';
import { addMovie } from "../../../../redux/actions/favouriteMovie";
import store from "../../../../redux";
const MoviesContext = React.createContext();
const { Provider } = MoviesContext;
const Movie = (props, {addMovie, movies}) => {
const [active, setActive] = useState(false)
let onClick = (e) => {
e.preventDefault();
console.log('hello');
setActive(!active);
addMovie(movies);
}
return (
<Provider store={{movies, addMovie}}>
<Link to= {"/modal/:"+`${props.id}`} id={props.id} className={'movie'}>
<img src={"https://image.tmdb.org/t/p/w500/"+props.poster_path} alt={props.title} className={'main'}/>
<h1 className={'title'}>{props.title}</h1>
<div>
<svg className={active ? 'activeimg' : 'heart'} onClick={onClick} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 4.248c-3.148-5.402-12-3.825-12 2.944 0 4.661 5.571 9.427 12 15.808 6.43-6.381 12-11.147 12-15.808 0-6.792-8.875-8.306-12-2.944z"/></svg>
<p className={'text'}>{props.release_date}</p>
</div>
</Link>
</Provider>
);
};
export {
MoviesContext
};
function mapStateToProps(state){
return {
movies: state.movie.movies
}
}
const mapDispatchToProps = {
addMovie: addMovie,
}
export default connect(mapStateToProps, mapDispatchToProps)(Movie);
I don't understand how to do it in component.
Thank you very much for your help!
You need to change your "mapDispatchToProps" to have a dispatch as params
but why this mixt between context and redux ?
import React, { useEffect, useState } from 'react';
import './index.css';
import { Link, useParams } from 'react-router-dom';
import { connect } from 'react-redux';
import { addMovie } from '../../../../redux/actions/favouriteMovie';
import store from '../../../../redux';
const MoviesContext = React.createContext();
const { Provider } = MoviesContext;
const Movie = (props, { addMovie, movies }) => {
const [active, setActive] = useState(false);
const onClick = (id) => {
console.log('hello');
setActive(!active);
props.addMovie(id);
};
return (
<Provider store={{ movies, addMovie }}>
<Link to={'/modal/:' + `${props.id}`} id={props.id} className={'movie'}>
<img src={'https://image.tmdb.org/t/p/w500/' + props.poster_path} alt={props.title} className={'main'} />
<h1 className={'title'}>{props.title}</h1>
<div>
<svg className={active ? 'activeimg' : 'heart'} onClick={() => onClick(props.id)} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 4.248c-3.148-5.402-12-3.825-12 2.944 0 4.661 5.571 9.427 12 15.808 6.43-6.381 12-11.147 12-15.808 0-6.792-8.875-8.306-12-2.944z" />
</svg>
<p className={'text'}>{props.release_date}</p>
</div>
</Link>
</Provider>
);
};
export { MoviesContext };
function mapStateToProps(state) {
return {
movies: state.movie.movies,
};
}
const mapDispatchToProps = {
addMovie: addMovie,
};
const mapDispatchToProps = (dispatch) => {
return {
addMovie: (id) => dispatch(addMovie(id)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Movie);
I'm trying to build a simple blog native app using context and have stumbled upon an issue to which I can't find a root to.
Here's the structure of it:
/context/createDataContext.js file:
import React, { useReducer } from "react";
export default (reducer, actions, initialState) => {
const Context = React.createContext();
const Provider = ({ childern }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const boundActions = {};
for (let key in boundActions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{childern}
</Context.Provider>
);
};
return { Context, Provider };
};
/context/BlogContext.js:
import createDataContext from "./createDataContext";
const blogReducer = (state, action) => {
switch (action.type) {
case "add_blogpost":
return [...state, { title: `Blog Post Number ${state.length + 1}` }];
default:
return state;
}
};
const addBlogPost = (dispatch) => {
return () => {
dispatch({ type: "add_blogpost" });
};
};
export const { Context, Provider } = createDataContext(
blogReducer,
{ addBlogPost },
[]
);
/screens/IndexScreen.js :
import React, { useContext } from "react";
import { View, Text, StyleSheet, FlatList, Button } from "react-native";
import { Context } from "../context/BolgContext";
const IndexScreen = () => {
const { state, addBlogPost } = useContext(Context);
return (
<View>
<Button title="Add a blod post" onPress={addBlogPost} />
<FlatList
data={state}
keyExtractor={(blogPost) => blogPost.title}
renderItem={({ item }) => {
return <Text>{item.title}</Text>;
}}
/>
</View>
);
};
const styles = StyleSheet.create({});
export default IndexScreen;
And finally App.js :
import { NavigationContainer } from "#react-navigation/native";
import { createStackNavigator } from "#react-navigation/stack";
import IndexScreen from "./src/screens/IndexScreen";
import { Provider } from "./src/context/BolgContext";
import React from "react";
const Stack = createStackNavigator();
export default function App() {
return (
<NavigationContainer>
{
<Provider>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={IndexScreen}
options={{ title: "My app" }}
/>
</Stack.Navigator>
</Provider>
}
</NavigationContainer>
);
}
Now I did some debugging, even though the code does't come back with any error, but the issue seems to be on my Provider, since if I remove it I can see content on the screen. Does anybody know why this happens.
Thanks a lot!
You need to change the Provider method like below
Form
const Provider = ({ childern }) => {
To
const Provider = (props) => {
Then you can destructure while passing to the content.provider like below
<Context.Provider value={{ state, ...boundActions }}>
{props.childern}
</Context.Provider>
I've created a simple to do app using React. I've attempted to persist state using local storage. However, the local storage code I've added is somehow preventing my components from rendering altogether. Not only are the todos saved in state not appearing, none of my components will render. I get a blank page on refresh. Can someone help me figure out what's wrong?
Here's what happens on the initial save after the local storage code is included. It loads the components just fine, but the to dos that are already in state are not shown:
After using the form to add to dos and refreshing the page, this happens. None of the components are shown whatsoever. Just a blank page.
Here is the local storage code inside my index.js file. I'm pretty sure the problem is here but I have included the code for the other components and the reducer as well:
const persistedState = localStorage.getItem('state') ? JSON.parse(localStorage.getItem('state')) : [];
const store = createStore(reducer, persistedState);
store.subscribe(() => {
localStorage.setItem('state', JSON.stringify(store.getState()));
})
The index.js file in its entirety:
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from "redux";
import { Provider } from "react-redux";
import './index.css';
import App from './App';
import { reducer } from "./reducers/todoReducer";
import * as serviceWorker from './serviceWorker';
const persistedState = localStorage.getItem('state') ? JSON.parse(localStorage.getItem('state')) : [];
const store = createStore(reducer, persistedState);
store.subscribe(() => {
localStorage.setItem('state', JSON.stringify(store.getState()));
})
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
the other components:
TodoList.js:
import Todo from "./Todo";
const TodoList = props => {
return (
<ul className="task-list">
{props.state.map(task => (
<Todo task={task} />
))}
</ul>
)
}
const mapStateToProps = state => {
return {
state: state
}
}
export default connect(mapStateToProps)(TodoList);
TodoForm.js:
const TodoForm = props => {
const [newItemText, setNewItemText] = useState("");
const handleChanges = e => {
e.preventDefault();
setNewItemText(e.target.value);
};
const saveState = () => localStorage.setItem("props.state", JSON.stringify(props.state));
useEffect(() => {
const todos = localStorage.getItem('state');
if (todos) props.setState({ [props.state]: JSON.parse(props.state) })
}, [])
return (
<div className="form-div">
<input
className="add-input"
name="todo"
type="text"
placeholder="enter a task"
value={newItemText}
onChange={handleChanges}
/>
<button
className="add-button"
onClick = {e => {
e.preventDefault();
props.addItem(newItemText);
saveState();
}}>Add a Task
</button>
<button
className="add-button"
onClick={e => {
e.preventDefault();
props.removeCompleted();
}}>Remove Completed
</button>
</div>
)
}
const mapStateToProps = state => {
return {
state: state
}
}
export default connect(mapStateToProps, {addItem, removeCompleted})(TodoForm);
Todo.js:
const Todo = props => {
return (
<li
className="tasks"
style={{textDecoration: props.task.completed ? 'line-through' : 'none'}}
onClick={() => props.toggleCompleted(props.task.id)}>
{props.task.item}
</li>
)
}
const mapStateToProps = state => {
return {
state: state
}
}
export default connect(mapStateToProps, {toggleCompleted})(Todo);
todoReducer.js:
export const initialState = [
{ item: 'Learn about reducers', completed: false, id: 1 },
{ item: 'review material from last week', completed: false, id: 2 },
{ item: 'complete reducer todo project', completed: false, id: 3 }
]
export const reducer = (state = initialState, action) => {
switch(action.type) {
case ADD_ITEM:
// console.log(action.payload)
return [
...state,
{
item: action.payload,
completed: false,
id: Date.now()
}
]
case TOGGLE_COMPLETED:
const toggledState = [...state];
toggledState.map(item => {
if(item.id === action.payload) {
item.completed = !item.completed;
}
})
console.log(toggledState);
state = toggledState;
return state;
case REMOVE_COMPLETED:
return state.filter(item => !item.completed);
default:
return state;
}
}
export default reducer;
App.js:
import React from 'react';
import './App.css';
// components
import TodoList from "./components/TodoList";
import TodoForm from "./components/TodoForm";
function App() {
return (
<div className="App">
<h1 className="title">To Do List</h1>
<TodoList />
<TodoForm />
</div>
);
}
export default App;
actions.js:
export const ADD_ITEM = 'ADD_ITEM';
export const TOGGLE_COMPLETED = 'TOGGLE_COMPLETED';
export const REMOVE_COMPLETED = 'REMOVE_COMPLETED';
export const addItem = input => {
return {
type: ADD_ITEM, payload: input
}
};
export const toggleCompleted = (id) => {
return {
type: TOGGLE_COMPLETED, payload: id
}
};
export const removeCompleted = () => {
return {
type: REMOVE_COMPLETED
}
};
I have two pages; the first one called QuizHomePage and which contains a welcome message and a button which allows to user to start a quiz.
QuizHomePage.tsx:
import Button from "#material-ui/core/Button";
import { createStyles, makeStyles, Theme } from "#material-ui/core/styles";
import Typography from "#material-ui/core/Typography";
import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { questionRequest, startQuiz } from "../../actions/index";
import AppBar from "../../components/common/AppBar";
import history from "../../history/history";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
textAlign: "center",
margin: theme.spacing(10)
},
button: {
marginTop: theme.spacing(6)
}
}));
interface IProps {
questionRequest: () => void;
startQuiz: () => void;
}
const QuizHomePage = (props: IProps) => {
const classes = useStyles();
const { questionRequest, startQuiz } = props;
const handleStartQuiz = () => {
questionRequest();
startQuiz();
return history.push("/contentQuiz");
};
return (<>
<AppBar />
<div className={classes.root}>
<Typography
color="textPrimary"
gutterBottom
variant="h2">
Test your javascript skills
</Typography>
<Typography
color="textSecondary"
gutterBottom
variant="h6">
Please click the start button to launch the Quiz
</Typography>
<Button
className={classes.button}
color="secondary"
onClick={handleStartQuiz}
variant="contained">Start</Button>
</div>
</>);
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
startQuiz: () => dispatch(startQuiz()),
questionRequest: () => dispatch<any>(questionRequest())
};
};
export default connect(null, mapDispatchToProps)(QuizHomePage);
When I click the Start button I dispatch 2 actions questionRequest which executes a promise and return the list of all questions from the database and startQuiz which dispatch an action to update the state of the quiz, then the user will be redirected to the quiz question page which described by this code:
import { Typography } from "#material-ui/core";
import React from "react";
import { connect } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { incrementQuestion, IQuestion } from "../../actions/index";
import ContentQuiz from "../../components/ContentQuiz";
interface IProps {
currentQuestionNumber: number;
questions: IQuestion[];
}
const QuizzContainer = (props: IProps) => {
const { currentQuestionNumber, questions } = props;
const currentQuestion = questions[currentQuestionNumber];
const handleNextQuiz = () => {
incrementQuestion();
};
return (
<ContentQuiz
questionNumber={currentQuestionNumber}
handleClick={handleNextQuiz}>
<div>
<Typography variant="h3" gutterBottom> What's the output of </Typography>
<>
<SyntaxHighlighter language="javascript" style={dark}>
{currentQuestion.questionDescription}
</SyntaxHighlighter>
</>
</div>
</ContentQuiz>
);
};
const mapStateToProps = (state: any) => {
const { currentQuestionNumber, questions } = state.quiz;
return {
currentQuestionNumber,
questions
};
};
export default connect(mapStateToProps, { incrementQuestion })(QuizzContainer);
actions.ts:
export const questionRequest = (): ThunkAction<void, AppState, null, Action<string>> => {
return async (dispatch: Dispatch) => {
dispatch(startQuestionRequest());
getQuestionsApi().then((response: AxiosResponse) => {
const { data } = response;
dispatch(questionSuccess(data.result));
},
(error: AxiosError) => {
let errorMessage = "Internal Server Error";
if (error.response) {
errorMessage = error.response.data.error;
}
dispatch(questionFail(errorMessage));
dispatch(errorAlert(errorMessage));
});
};
};
I got an error :
TypeError: Cannot read property 'questionDescription' of undefined
it's normally because for react the questionsvariable is undefined. I realized that the questions array is not updated quickly but after some amount of time due to the server response that's why the QuizzContainerreturns the error mentioned below when it tries to mount the component.
Is it a good approach to lazy load the component in order to wait the fetching of questions from server and then mounting the QuizContainer component? I trieduseEffectwhich normally behaves ascomponentDidMount` but it does not work with my issue.
How can I fix that?
You need to use async and await here. If you don't wait until the promise gets resolved and navigate the user to the next page, you can never guarantee that the user will see the question as soon as page loads.
const handleStartQuiz = async () => {
awit questionRequest();
await startQuiz();
return history.push("/contentQuiz");
}
Second approach: (I don't recommend)
Don't render the question unless you have questions filled in the redux state.
return(
{ questions && <ContentQuiz> ... </ContentQuiz> }
)
I resolved my question using this update:
import { Typography } from "#material-ui/core";
import React from "react";
import { connect } from "react-redux";
import SyntaxHighlighter from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { incrementQuestion, IQuestion } from "../../actions/index";
import ContentQuiz from "../../components/ContentQuiz";
interface IProps {
currentQuestionNumber: number;
loadingData: boolean;
questions: IQuestion[];
questionRequest: () => void;
}
const QuizzContainer = (props: IProps) => {
const { currentQuestionNumber, loadingData, questions, questionRequest } = props;
useEffect(() => {
questionRequest();
});
const currentQuestion = questions[currentQuestionNumber];
const handleNextQuiz = () => {
incrementQuestion();
};
return (
<div>
{loadingData ? ("Loading ...") : (
<ContentQuiz
questionNumber={currentQuestionNumber}
handleClick={handleNextQuiz}>
<div>
<Typography variant="h3" gutterBottom> What's the output of </Typography>
<>
<SyntaxHighlighter language="javascript" style={dark}>
{currentQuestion.questionDescription}
</SyntaxHighlighter>
</>
</div>
</ContentQuiz>
)}
</div>
);
};
const mapStateToProps = (state: any) => {
const { currentQuestionNumber, loadingData, questions } = state.quiz;
return {
currentQuestionNumber,
loadingData,
questions
};
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
incrementQuestion: () => dispatch(incrementQuestion()),
questionRequest: () => dispatch<any>(questionRequest())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(QuizzContainer);