React/Redux Reset State Upon Route Change - javascript

EDIT: In the code I show below the user NEVER sees the message because the state is reset. The screenshot is merely an example to show the message that I'd like to get displayed.
Current State: Upon successful add to the database, "Facility created successfully" message NEVER shows. However, if I remove the reset code, when the user navigates away from the page and comes back, the message persists. The root cause of this is that my state in Redux has a "success" setting upon successful add to the database. If I do a reset upon the success message, the user never sees it because the state is reset to an empty object.
Ideal State: User adds facility to database, success message comes back via Redux state and message shows. The page refreshes showing the newly added facility. The user navigates away from the page and then the Redux state is reset.
I've tried libraries from React to detect unmount in addition to using a cleanup function in useEffect to no avail.
I will list my code below and appreciate any feedback or insights. Note that some irrelevant code has been trimmed for the sake of space.
FacilityAdminScreen.js (component)
import React, { useState, useEffect } from "react";
import { Form, Row, Button, Col } from "react-bootstrap";
import { useDispatch, useSelector } from "react-redux";
import Message from "../components/Message";
import Loader from "../components/Loader";
import { chunk } from "lodash";
import FacilityCard from "../components/FacilityCard";
import { createFacility, getFacilityAllBasicInfo } from "../actions/facilityActions";
import { FACILITY_ADD_TO_DATABASE_RESET } from "../constants/facilityConstants";
const FacilityAdminScreen = ({ match }) => {
// State for form information
const [name, setName] = useState("");
const [streetAddress1, setStreetAddress1] = useState("");
const [streetAddress2, setStreetAddress2] = useState("");
const [city, setCity] = useState("");
const [state, setState] = useState("");
const [zip, setZip] = useState("");
const dispatch = useDispatch();
// List of facilities for populating page
const facilityAllBasicInfo = useSelector(state => state.facilityAllBasicInfo);
const { loading, error } = facilityAllBasicInfo;
const facilities = chunk(facilityAllBasicInfo.facilities, 2);
// Response upon adding facility to database
const facilityAddToDatabase = useSelector(state => state.facilityAddToDatabase);
const {
loading: loadingCreate,
error: errorCreate,
success: successCreate,
} = facilityAddToDatabase;
const submitHandler = e => {
e.preventDefault();
// Attempt to create the facility
dispatch(
createFacility({
company: match.params.companyId,
name,
streetAddress1,
streetAddress2,
city,
state,
zip,
})
);
};
useEffect(() => {
// Get all facilities for company
dispatch(getFacilityAllBasicInfo(match.params.companyId));
// If facility created successfully, reset all form state
if (successCreate) {
dispatch({ type: FACILITY_ADD_TO_DATABASE_RESET });
setName("");
setStreetAddress1("");
setStreetAddress2("");
setCity("");
setState("");
setZip("");
}
}, [dispatch, successCreate, match.params.companyId]);
return (
<>
<Row>
<Col md={8}>
<h1>Facilities</h1>
<h6>Click facility name for detailed information</h6>
{loading ? (
<Loader />
) : error ? (
<Message variant="danger">{error}</Message>
) : (
<>
{facilities.map((facilityArray, i) => (
<Row className="mb-3" key={i}>
{facilityArray.map((facility, i) => (
<Col md={6} key={i}>
<FacilityCard
key={facility._id}
companyId={match.params.companyId}
id={facility._id}
name={facility.name}
streetAddress1={facility.streetAddress1}
streetAddress2={facility.streetAddress2}
city={facility.city}
state={facility.state}
zip={facility.zip}
isActive={facility.isActive}
/>
</Col>
))}
</Row>
))}
</>
)}
</Col>
<Col md={4}>
<h1>Add Facility</h1>
{loadingCreate && <Loader />}
{errorCreate && <Message variant="danger">{errorCreate}</Message>}
{successCreate && (
<Message variant="success">Facility created successfully</Message>
)}
<Form onSubmit={submitHandler}>
{/*Trimmed code here for the sake of space */}
</Form>
</Col>
</Row>
</>
);
};
export default FacilityAdminScreen;
Reducers for BOTH getting the list of facilities and adding to database:
export const facilityAllBasicInfoReducer = (state = { facilities: [] }, action) => {
switch (action.type) {
case FACILITY_ALL_BASIC_INFO_REQUEST:
return { loading: true };
case FACILITY_ALL_BASIC_INFO_SUCCESS:
return { loading: false, facilities: action.payload };
case FACILITY_ALL_BASIC_INFO_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
export const facilityAddToDatabaseReducer = (state = {}, action) => {
switch (action.type) {
case FACILITY_ADD_TO_DATABASE_REQUEST:
return { loading: true };
case FACILITY_ADD_TO_DATABASE_SUCCESS:
return { loading: false, success: true, facility: action.payload };
case FACILITY_ADD_TO_DATABASE_FAIL:
return { loading: false, error: action.payload };
case FACILITY_ADD_TO_DATABASE_RESET:
return {};
default:
return state;
}
};

I don't think that your implementation of a cleanup function within useEffect that you copied in above is the right way to do it. The issue you are having is that the dispatch to reset the state is fired off straight after success becomes true.
I think what you are looking for is a way to use componentWillUnmount using Hooks, so that when you navigate away from the page and the component is destroyed it fires off an action that modifies the state the way you want.
The return statement on useEffect does this. Whatever you include in the return statement will be fired off when the component unmounts.
useEffect(() => {
dispatch(//action to add facility);
return () => {
dispatch(//action to modify state when component unmounts);
};
}, [dispatch]);
Using the above, when you navigate back to the page success will again be false.
Hopefully this helps.

Related

Supabase onAuthStateChanged - How do I properly wait for the request to finish prevent flickering with useEffect?

Everything auth-wise is working fine. I even have a loading state setup so that the loader shows until the state is changed, but I still get this flickering on reload. This flickering only happens with Supabase. I was using the Firebase version before and it worked perfectly with my code.
Here is a video for reference: https://imgur.com/a/5hywXj5
Edit: Updated code to current version
export default function Navigation() {
const { user, setUser } = useContext(AuthenticatedUserContext);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const session = supabase.auth.session();
setUser(session?.user ?? null);
const { data: listener } = supabase.auth.onAuthStateChange((_: any, session: any) => {
setUser(session?.user ?? null);
});
setIsLoading(false);
return () => {
listener?.unsubscribe();
};
}, []);
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator color={Theme.colors.purple} size="large" />
</View>
);
}
return (
<NavigationContainer linking={LinkingConfiguration}>{user ? <AppStack /> : <AuthStack />}</NavigationContainer>
);
}
To recap for others, onAuthStateChange will not execute on first page load so you are triggering it using the getUserAuthStatus async function. However session() function is not async and will immediately return a result of null if there is no user session, or return a session that has been stored in localStorage.
In this case the result of the getUserAuthStatus will always return null. Then onAuthStateChange will trigger with the SIGNED_IN event and a session which will then set the user.
Furthermore the onAuthStateChange function should be registered before you perform the session step so as to capture any events triggered. In the current form an event may be triggered directly after the session() call but before the handler is registered.
So to recap the rendering steps will be:
Step 1
isLoading: true
user: null
Step 2
isLoading: false
user: null
Step 3
isLoading false
user: {...}
So far as I can tell, using session directly without thinking it's async will do the trick.
Ok, Supabase has released some updates since I first asked this question. Here is how I am now able to stop flickering when loading the application.
First, we need to set up our AuthContext for our application. Be sure to wrap your App.tsx with the <AuthContextProvider>.
AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { Session, User } from '#supabase/supabase-js';
import { supabase } from '../config/supabase';
export const AuthContext = createContext<{ user: User | null; session: Session | null }>({
user: null,
session: null,
});
export const AuthContextProvider = (props: any) => {
const [userSession, setUserSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUserSession(session);
setUser(session?.user ?? null);
});
const { data: authListener } = supabase.auth.onAuthStateChange(async (event, session) => {
console.log(`Supabase auth event: ${event}`);
setUserSession(session);
setUser(session?.user ?? null);
});
return () => {
authListener.subscription;
};
}, []);
const value = {
userSession,
user,
};
return <AuthContext.Provider value={value} {...props} />;
};
export const useUser = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useUser must be used within a AuthContextProvider.');
}
return context;
};
Now, if you're using React Navigation like me we need to check if we have a valid user to send them to the logged-in home screen. Here's how I do it.
Navigation.tsx
export default function Navigation() {
const { user } = useUser();
return (
<NavigationContainer linking={LinkingConfiguration}>
{user ? <AppStackNavigator /> : <AuthStackNavigator />}
</NavigationContainer>
);
}

Making an axios get request and using React useState but when logging the data it still shows null

When I make a request to an API and setting the state to the results from the Axios request it still shows up null. I am using React useState and setting the results from the request and wanting to check to see if its coming through correctly and getting the right data its still resulting into null. The request is correct but when I use .then() to set the state that is the issue I am having.
Below is the component that I am building to make the request called Details.js (first code block) and the child component is the DetailInfo.js file (second code block) that will be displaying the data. What am I missing exactly or could do better when making the request and setting the state correctly display the data?
import React, {useEffect, useState} from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import axios from 'axios';
import { getCookie } from '../utils/util';
import DetailInfo from '../components/DetailInfo';
import DetailImage from '../components/DetailImage';
const Details = () => {
const [ countryData, setCountryData ] = useState(null);
let country;
let queryURL = `https://restcountries.eu/rest/v2/name/`;
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL)
.then((res) => {
console.log(res.data[0])
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData)
}
);
}, [])
return (
<>
<Container className="details">
<Row>
<Col sm={6}>
<DetailImage />
</Col>
<Col sm={6}>
<DetailInfo
name={countryData.name}
population={countryData.population}
region={countryData.region}
subRegion={countryData.subRegion}
capital={countryData.capital}
topLevelDomain={countryData.topLevelDomain}
currencies={countryData.currencies}
language={countryData.language}
/>
</Col>
</Row>
</Container>
</>
)
}
export default Details;
The child component below......
import React from 'react';
const DetailInfo = (props) => {
const {name, population, region, subRegion, capital, topLevelDomain, currencies, language} = props;
return (
<>detail info{name}{population} {region} {capital} {subRegion} {topLevelDomain} {currencies} {language}</>
)
}
export default DetailInfo;
Ultimately, the problem comes down to not handling the intermediate states of your component.
For components that show remote data, you start out in a "loading" or "pending" state. In this state, you show a message to the user saying that it's loading, show a Spinner (or other throbber), or simply hide the component. Once the data is retrieved, you then update your state with the new data. If it failed, you then update your state with information about the error.
const [ dataInfo, setDataInfo ] = useState(/* default dataInfo: */ {
status: "loading",
data: null,
error: null
});
useEffect(() => {
let unsubscribed = false;
fetchData()
.then((response) => {
if (unsubscribed) return; // unsubscribed? do nothing.
setDataInfo({
status: "fetched",
data: response.data,
error: null
});
})
.catch((err) => {
if (unsubscribed) return; // unsubscribed? do nothing.
console.error('Failed to fetch remote data: ', err);
setDataInfo({
status: "error",
data: null,
error: err
});
});
return () => unsubscribed = true;
}, []);
switch (dataInfo.status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {dataInfo.error.message}
</div>
);
}
// render data using dataInfo.data
return (
/* ... */
);
If this looks like a lot of boiler plate, there are useAsyncEffect implementations like #react-hook/async and use-async-effect that handle it for you, reducing the above code to just:
import {useAsyncEffect} from '#react-hook/async'
/* ... */
const {status, error, value} = useAsyncEffect(() => {
return fetchData()
.then((response) => response.data);
}, []);
switch (status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {error.message}
</div>
);
}
// render data using value
return (
/* ... */
);
Because state only update when component re-render. So you should put console.log into useEffect to check the new value:
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL).then(res => {
console.log(res.data[0]);
setCountryData(res.data[0]);
});
}, []);
useEffect(() => {
console.log(countryData);
}, [countryData]);
useState does reflecting its change immediately.
I think that it would be probably solved if you set countryData to second argument of useEffect.
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL)
.then((res) => {
console.log(res.data[0])
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData)
}
);
}, [countryData])
The issue is, as samthecodingman, pointed out, an issue of intermediate data. Your component is being rendered before the data is available, so your child component needs to re-render when its props change. This can be done via optional chaining, an ES6 feature.
import React, { useEffect, useState } from "react";
import DetailInfo from "./DetailInfo";
import { Col, Container, Row } from "react-bootstrap";
import axios from "axios";
const Details = () => {
const [countryData, setCountryData] = useState({});
let country = "USA";
let queryURL = `https://restcountries.eu/rest/v2/name/`;
useEffect(() => {
console.log(country);
queryURL += country;
console.log(queryURL);
axios
.get(queryURL)
.then((res) => {
console.log(res.data[0]);
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData);
});
}, []);
return (
<Container className="details">
<Row>
<Col sm={6}>
<DetailInfo
name={countryData?.name}
population={countryData?.population}
region={countryData?.region}
subRegion={countryData?.subRegion}
capital={countryData?.capital}
language={countryData?.language}
/>
</Col>
<Col sm={6}></Col>
</Row>
</Container>
);
};
export default Details;
Checkout my Codesandbox here for an example.

How to properly pass useReducer actions down to children without causing unnecessary renders

I can't quite figure out the optimal way to use useReducer hook for data management. My primary goal is to reduce (heh) the boilerplate to minimum and maintain code readability, while using the optimal approach in terms of performance and preventing unnecessary re-renders.
The setup
I have created a simplified example of my app, basically it's a <List /> component - a list of items with possibility to select them, and a <Controls /> component which can switch item groups and reload the data set.
List.js
import React, { memo } from "react";
const List = ({ items, selected, selectItem, deselectItem }) => {
console.log("<List /> render");
return (
<ul className="List">
{items.map(({ id, name }) => (
<li key={`item-${name.toLowerCase()}`}>
<label>
<input
type="checkbox"
checked={selected.includes(id)}
onChange={(e) =>
e.target.checked ? selectItem(id) : deselectItem(id)
}
/>
{name}
</label>
</li>
))}
</ul>
);
};
export default memo(List);
Controls.js
import React, { memo } from "react";
import { ItemGroups } from "./constants";
const Controls = ({ group, setGroup, fetchItems }) => {
console.log("<Controls /> render");
return (
<div className="Controls">
<label>
Select group
<select value={group} onChange={(e) => setGroup(e.target.value)}>
<option value={ItemGroups.PEOPLE}>{ItemGroups.PEOPLE}</option>
<option value={ItemGroups.TREES}>{ItemGroups.TREES}</option>
</select>
</label>
<button onClick={() => fetchItems(group)}>Reload data</button>
</div>
);
};
export default memo(Controls);
App.js
import React, { useEffect, useReducer } from "react";
import Controls from "./Controls";
import List from "./List";
import Loader from "./Loader";
import { ItemGroups } from "./constants";
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import fetchItemsFromAPI from "./api";
import "./styles.css";
const itemsReducer = (state, action) => {
const { type, payload } = action;
console.log(`reducer action "${type}" dispatched`);
switch (type) {
case FETCH_START:
return {
...state,
isLoading: true
};
case FETCH_SUCCESS:
return {
...state,
items: payload.items,
isLoading: false
};
case SET_GROUP:
return {
...state,
selected: state.selected.length ? [] : state.selected,
group: payload.group
};
case SELECT_ITEM:
return {
...state,
selected: [...state.selected, payload.id]
};
case DESELECT_ITEM:
return {
...state,
selected: state.selected.filter((id) => id !== payload.id)
};
default:
throw new Error("Unknown action type in items reducer");
}
};
export default function App() {
const [state, dispatch] = useReducer(itemsReducer, {
items: [],
selected: [],
group: ItemGroups.PEOPLE,
isLoading: false
});
const { items, group, selected, isLoading } = state;
const fetchItems = (group) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
const setGroup = (group) => {
dispatch({
type: SET_GROUP,
payload: { group }
});
};
const selectItem = (id) => {
dispatch({
type: SELECT_ITEM,
payload: { id }
});
};
const deselectItem = (id) => {
dispatch({
type: DESELECT_ITEM,
payload: { id }
});
};
useEffect(() => {
console.log("use effect on group change");
fetchItems(group);
}, [group]);
console.log("<App /> render");
return (
<div className="App">
<Controls {...{ group, fetchItems, setGroup }} />
{isLoading ? (
<Loader />
) : (
<List {...{ items, selected, selectItem, deselectItem }} />
)}
</div>
);
}
Here's the complete sandbox.
The state is managed in a reducer, because I need different parts of state to work and change together. For example, reset selected items on group change (because it makes no sense to keep selections between different data sets), set loaded items and clear loading state on data fetch success, etc. The example is intentionally simple, but in reality there're many dependencies between different parts of state (filtering, pagination, etc.), which makes reducer a perfect tool to manage it - in my opinion.
I've created helper functions to perform different actions (for ex., to reload items or to select/deselect). I could just pass down the dispatch to children and create action objects there, but this turns everything into a mess really quickly, esp. when multiple components must perform same actions.
Problem 1
Passing down reducer action functions to child components causes them to re-render on any reducer update.
Case 1: When I select an item in <List />, the <Controls /> is
re-rendered.
Case 2: When I reload the data on Reload button click, the <Controls /> is
re-rendered.
In both cases, the <Controls /> only actually depends on group prop to render, so when it stays the same - the component should not re-render.
I've investigated it and this happens because on each <App /> re-render these action functions are re-created and treated as new prop values for child components, so for React it's simple: new props => new render.
Not ideal solution to this is to wrap all action functions in useCallback, with dispatch as a dependency, but this looks like a hack to me.
const setGroup = useCallback(
(group) => {
dispatch({
type: SET_GROUP,
payload: { group }
});
},
[dispatch]
);
In a simple example it does not look too bad, but when you have dozens of possible actions, all wrapped in useCallback, with deps arrays - that does not seem right.
And it requires to add even more deps to useEffect (which is another problem).
Here's a "fixed" version with useCallback.
Problem 2
I cannot fully extract reducer action functions outside the <App /> component, because in the end they must be used inside a React component with the dispatch (because it's a hook).
I can of course extract them to a separate module and pass dispatch as a first argument:
in actions.js
// ...
export const fetchItems = (dispatch, group) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
// ...
and then in child components do this:
import { fetchItems } from './actions';
const Child = ({ dispatch, group }) => {
fetchItems(dispatch, group);
// ...
};
and reduce my <App /> to this:
// ...
const App = () => {
const [{ items, group, selected, isLoading }, dispatch] = useReducer(
itemsReducer,
itemReducerDefaults
);
useEffect(() => {
fetchItems(dispatch, group);
}, [group, dispatch]);
return (
<div className="App">
<Controls {...{ group, dispatch }} />
{isLoading ? <Loader /> : <List {...{ items, selected, dispatch }} />}
</div>
);
};
but then I have to pass around the dispatch (minor issue) and always have it in arguments list. On the other hand, it fixes the Problem 1 as well, as dispatch does not change between renders.
Here's a sandbox with actions and reducer extracted.
But is it optimal, or maybe I should use some entirely different approach?
So, how do you guys use it? The React docs and guides are nice and clean with counter increments and ToDo lists, but how do you actually use it in real world apps?
React-redux works by also wrapping all the actions with a call to dispatch; this is abstracted away when using the connect HOC, but still required when using the useDispatch hook. Async actions typically have a function signature (...args) => dispatch => {} where the action creator instead returns a function that accepts the dispatch function provided by redux, but redux requires middleware to handle these. Since you are not actually using Redux you'd need to handle this yourself, likely using a combination of both patterns to achieve similar usage.
I suggest the following changes:
De-couple and isolate your action creators, they should be functions that return action objects (or asynchronous action functions).
Create a custom dispatch function that handles asynchronous actions.
Correctly log when a component renders (i.e. during the commit phase in an useEffect hook and not during any render phase in the component body. See this lifecycle diagram.
Pass the custom dispatch function to children, import actions in children... dispatch actions in children. How to avoid passing callbacks down.
Only conditionally render the Loader component. When you render one or the other of Loader and List the other is unmounted.
Actions (actions.js)
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import fetchItemsFromAPI from "./api";
export const setGroup = (group) => ({
type: SET_GROUP,
payload: { group }
});
export const selectItem = (id) => ({
type: SELECT_ITEM,
payload: { id }
});
export const deselectItem = (id) => ({
type: DESELECT_ITEM,
payload: { id }
});
export const fetchItems = (group) => (dispatch) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
useAsyncReducer.js
const asyncDispatch = (dispatch) => (action) =>
action instanceof Function ? action(dispatch) : dispatch(action);
export default (reducer, initialArg, init) => {
const [state, syncDispatch] = React.useReducer(reducer, initialArg, init);
const dispatch = React.useMemo(() => asyncDispatch(syncDispatch), []);
return [state, dispatch];
};
Why doesn't useMemo need a dependency on useReducer dispatch function?
useReducer
Note
React guarantees that dispatch function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
We want to also provide a stable dispatch function reference.
App.js
import React, { useEffect } from "react";
import useReducer from "./useAsyncReducer";
import Controls from "./Controls";
import List from "./List";
import Loader from "./Loader";
import { ItemGroups } from "./constants";
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import { fetchItems } from "./actions";
export default function App() {
const [state, dispatch] = useReducer(itemsReducer, {
items: [],
selected: [],
group: ItemGroups.PEOPLE,
isLoading: false
});
const { items, group, selected, isLoading } = state;
useEffect(() => {
console.log("use effect on group change");
dispatch(fetchItems(group));
}, [group]);
React.useEffect(() => {
console.log("<App /> render");
});
return (
<div className="App">
<Controls {...{ group, dispatch }} />
{isLoading && <Loader />}
<List {...{ items, selected, dispatch }} />
</div>
);
}
Controls.js
import React, { memo } from "react";
import { ItemGroups } from "./constants";
import { setGroup, fetchItems } from "./actions";
const Controls = ({ dispatch, group }) => {
React.useEffect(() => {
console.log("<Controls /> render");
});
return (
<div className="Controls">
<label>
Select group
<select
value={group}
onChange={(e) => dispatch(setGroup(e.target.value))}
>
<option value={ItemGroups.PEOPLE}>{ItemGroups.PEOPLE}</option>
<option value={ItemGroups.TREES}>{ItemGroups.TREES}</option>
</select>
</label>
<button onClick={() => dispatch(fetchItems(group))}>Reload data</button>
</div>
);
};
List.js
import React, { memo } from "react";
import { deselectItem, selectItem } from "./actions";
const List = ({ dispatch, items, selected }) => {
React.useEffect(() => {
console.log("<List /> render");
});
return (
<ul className="List">
{items.map(({ id, name }) => (
<li key={`item-${name.toLowerCase()}`}>
<label>
<input
type="checkbox"
checked={selected.includes(id)}
onChange={(e) =>
dispatch((e.target.checked ? selectItem : deselectItem)(id))
}
/>
{name}
</label>
</li>
))}
</ul>
);
};
Loader.js
const Loader = () => {
React.useEffect(() => {
console.log("<Loader /> render");
});
return <div>Loading data...</div>;
};

Strange issue when making requests/displaying data on React (Axios, React, Redux)

I'm trying to make a personal app and I'm running into some issues with the requests, nothing to do with the API.
The results of the request returns this JSON: https://pastebin.com/raw/vpdq2k6S
My code:
class Home extends Component {
componentDidMount() {
this.props.getTrending();
}
render() {
const { trending } = this.props.trending;
console.log(trending);
const Card = (props) => (
<Panel {...props} bordered header='Card title'>
{trending.results.map((i) => (
<React.Fragment key={i.id}>
<h6>{i.title}</h6>
</React.Fragment>
))}
</Panel>
);
return (
<div className='Home'>
<FlexboxGrid justify='center'>
<Card />
</FlexboxGrid>
</div>
);
}
}
export default connect(
(state) => {
return {
trending: state.trending,
};
},
{ getTrending }
)(Home);
My action:
import { GET_TRENDING } from "../types";
import axios from "axios";
export const getTrending = () => async (dispatch) => {
const res = await axios.get(
`https://api.themoviedb.org/3/movie/popular?api_key=KEY&language=en-US&page=1`
);
dispatch({
type: GET_TRENDING,
payload: res.data,
});
};
My reducer:
import { GET_TRENDING } from "../types";
const initialState = {
trending: [],
loading: true,
};
export default function trendingReducer(state = initialState, action) {
switch (action.type) {
case GET_TRENDING:
return {
...state,
trending: action.payload,
loading: false,
};
default:
return state;
}
}
Ignore the Card constant etc, that's related to an UI components library. You can see from the code that I'm logging the results of 'trending'. But I'm getting an empty array but here's the strange part:
If I change my map function from mapping through "trending.results" to "trending" and I refresh then the console returns an empty array and another array which is the correct one. If I change it back to "trending.results" then React auto-reloads the page returns me the correct array two times and it displays the data on the app but if I refresh the page without changing anything on the code then it goes back to showing an empty array and an error that says "cannot read property map of undefined" obviously cause somehow I'm not getting the correct data.
Anyone ever had this before? It makes absolutely no sense at all or if so then can someone guide me on how to solve this? I tried shutting down the React server completely and restarting it that wouldn't solve it. My brain is frozen (I can record a clip if required)
The answer is pretty simple. All you have to do is first getting the array from the api and then mapping through it. trending.results is not set so the error is shown. Cannot read property map of undefined
Go with a ternary operator:
{trending.results && trending.results.map((i) => (
<React.Fragment key={i.id}>
<h6>{i.title}</h6>
</React.Fragment>
))}
try this way.
export const getTrending = () => async (dispatch) => {
const res = await axios.get(
`https://api.themoviedb.org/3/movie/popular?api_key=KEY&language=en-US&page=1`
);
dispatch({
type: GET_TRENDING,
payload: res.data.results,
});
};
const Card = (props) => (
<Panel {...props} bordered header='Card title'>
{trending && trending.map((i) => (
<React.Fragment key={i.id}>
<h6>{i.title}</h6>
</React.Fragment>
))}
</Panel>
);

My reducer does not seem to modify the state in the store to allow my React view to render again

To begin, I should mention that I have followed this pattern I'm about to show you with two other sets of React components in my project.
For some reason it seems to not work and I have focused in on that either the #connect or the reducer is the problem. I've had several people look over this and I've attempted several different methods to solve this problem.
import classNames from 'classnames';
import SidebarMixin from 'global/jsx/sidebar_component';
import Header from 'common/header';
import Sidebar from 'common/sidebar';
import Footer from 'common/footer';
import AppList from '../components/lists';
import { connect } from 'react-redux'
import actions from 'redux/actions';
import { VisibilityFilters } from 'redux/actions/actionTypes';
#connect((state) => state)
class AppContainer extends React.Component {
constructor(props) {
super(props)
}
componentWillMount() {
this.props.dispatch(actions.getUnconsidered(1));
this.props.dispatch(actions.getConsidered(3));
this.props.dispatch(actions.getInterviews(4));
this.props.dispatch(actions.getOffers(2));
}
This right here is dispatching four actions to fetch data for the application. We are using Redux Thunk middleware to deal with the async problems associated with making these Ajax requests.
I have found that the data from all of these Ajax calls is reaching the reducer.
render() {
console.log("AppContainer state", this.props);
This console.log should log twice... once empty during the first render and again when the state changes after the reducers which should show the state with my data mapped to the props because of the #connect.
I'm fairly certain of this because of my other views that I've done.
return (
<div>
<Container id='body'>
<Grid>
<Row>
<Col md={3}>
<AppList data={this.props.unconsidered}/>
</Col>
<Col md={3}>
<AppList data={this.props.considered} />
</Col>
<Col md={3}>
<AppList data={this.props.interviews} />
</Col>
<Col md={3}>
<AppList data={this.props.offers} />
</Col>
</Row>
</Grid>
</Container>
</div>
)
}
}
#SidebarMixin
export default class extends React.Component {
render() {
const dispatch = this.props.dispatch
var classes = classNames({
'container-open': this.props.open
})
return (
<Container id='container' className={classes}>
<Sidebar />
<Header />
<AppContainer />
<Footer />
</Container>
)}
}
This next code block is my action creator file.
import axios from 'axios';
import {GET_UNCONSIDERED,
GET_CONSIDERED,
GET_INTERVIEWS,
GET_OFFERS } from './actionTypes';
function getUnconsidered(jobID){
console.log('getUnconsidered Actions')
return dispatch => axios.get('/user/employer/appsbystatus?jobID='+jobID+'&status=unconsidered')
.then(
payload => dispatch({ type: GET_UNCONSIDERED, payload})
)
.catch(resp => console.log("Error fetching unconsidered", resp));
}
function getConsidered(jobID){
return dispatch => axios.get('/user/employer/appsbystatus?jobID='+jobID+'&status=considered')
.then(
payload => dispatch({ type: GET_CONSIDERED, payload})
);
}
function getInterviews(jobID){
return dispatch => axios.get('/user/employer/appsbystatus?jobID='+jobID+'&status=interviews')
.then(
payload => dispatch({ type: GET_INTERVIEWS, payload})
);
}
function getOffers(jobID){
return dispatch => axios.get('/user/employer/appsbystatus?jobID='+jobID+'&status=offers')
.then(
payload => dispatch({ type: GET_OFFERS, payload})
);
}
module.exports = {
getUnconsidered: getUnconsidered,
getConsidered: getConsidered,
getInterviews: getInterviews,
getOffers: getOffers
}
This here is my reducer file and where I believe my problem lies.
import {GET_UNCONSIDERED,
GET_CONSIDERED,
GET_INTERVIEWS,
GET_OFFERS } from '../actions/actionTypes';
function empdashboard(state = {}, action) {
console.log("state in dashboard reducer = ", state);
switch (action.type) {
case GET_UNCONSIDERED:
console.log("unconsidered:", action.payload)
const unconsidered = action.payload.data;
return Object.assign({}, state, {
unconsidered: unconsidered
});
case GET_CONSIDERED:
console.log("considered:", action.payload)
const considered = action.payload.data;
return Object.assign({}, state, {
considered: considered
});
case GET_INTERVIEWS:
console.log("interviews:", action.payload)
const interviews = action.payload.data;
return Object.assign({}, state, {
interviews: interviews
});
case GET_OFFERS:
console.log("offers:", action.payload)
const offers = action.payload.data;
return Object.assign({}, state, {
offers: offers
});
default:
return state;
}
}
module.exports = {
empdashboard: empdashboard
}
Honestly I'm under the impression that either my reducer is messed up or I'm having a bug with my #connect and it is not detecting the change in state to rerender my view.
Thanks for taking a look at this!

Categories