I apologize for the ignorance on my part but I can't understand why this code works until the page is refreshed. The main goal is to utilize the array.reduce method on the response.json from my fetch so that I can have a rolling sum of all of the json[].amount element displayed in my component. This code works until the page is refreshed and then it says: Uncaught TypeError: Cannot read properties of null (reading 'reduce') at Balance (Balance.js:27:1). I know at the very least I'm doing something wrong with the state.
Balance.js component:
import { useEffect } from "react";
import {useBalancesContext} from '../hooks/useBalancesContext'
import { useAuthContext } from '../hooks/useAuthContext'
function Balance(){
const {balances, dispatch} = useBalancesContext()
const {user} = useAuthContext()
useEffect(() => {
const addBalances = async () => {
const response = await fetch("http://localhost:4000/api/balances", {
headers: {
'Authorization': `Bearer ${user.token}`
}
});
const json = await response.json();
if (response.ok) {
dispatch({type: 'SET_BALANCES', payload: json})
}
}
if (user) {
addBalances();
}
}, [dispatch, user]);
const newBalance = balances.reduce((accumulator, currentValue) => accumulator + currentValue.amount, 0)
return (
<div className="header">
<strong>Available Balance: ${newBalance}</strong>
</div>
)
}
export default Balance;
BalanceContext.js:
import { createContext, useReducer } from "react";
export const BalancesContext = createContext();
export const balancesReducer = (state, action) => {
switch (action.type) {
case 'SET_BALANCES':
return {
balances: action.payload
}
/* case 'SUM_BALANCES':
return {
balances: state.balances.amount.reduce((accumulator, currentValue) => accumulator + currentValue.amount, 0)
} */
case 'CREATE_BALANCE':
return {
balances: [action.payload, ...state.balances]
}
case 'DELETE_BALANCE':
return {
balances: state.balances.filter((b) => b._id !== action.payload._id)
}
default:
return state
}
}
export const BalancesContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(balancesReducer, {
balances: null
})
return (
<BalancesContext.Provider value={{...state, dispatch}}>
{children}
</BalancesContext.Provider>
)
};
I've tried using the array.reduce method inside of the function but then I don't have access to the newBalance value inside of Balance component. I've also tried using array.reduce in the reducer as well but I'm not confident I was on the right track with that either.
balances is initialially null, so its value may not be set when the page is loaded.
A good approach with values calculated with .reduce() is to use the useMemo hook:
import { useMemo } from 'react';
const newBalance = useMemo(() => {
if (!balances) return 0;
return balances.reduce(
(accumulator, currentValue) => accumulator + currentValue.amount,
0
);
}, [balances]);
This will tell React to calculate newBalance only when balances changes and will return 0 if balances is not already set.
Related
I have a NextJS application that is using the ShopifyBuy SDK. I have been successfully able to implement a solution where I am able to fetch the products from Store and display them to the User. The user is also able to go to a product page and add the product to the cart.
However, when the user refreshes the page, the cart is reset, and the data does not persist. The code is below:
context/cart.js:
import { createContext, useContext, useEffect, useReducer } from "react";
import client from "../lib/client";
import Cookies from "js-cookie";
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = "SET_CART";
const initalState = {
lineItems: [],
totalPrice: 0,
webUrl: "",
id: "",
};
const reducer = (state, action) => {
switch (action.type) {
case SET_CART:
return { ...state, ...action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
export const CartProvider = ({ children }) => {
const savedState = Cookies.get("cartState");
const [state, dispatch] = useReducer(reducer, savedState || initalState);
useEffect(() => {
Cookies.set("cartState", state, { expires: 7 });
}, [state]);
useEffect(() => {
getCart();
}, []);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
const getCart = async () => {
try {
const cart = await client.checkout.create();
setCart(cart);
} catch (err) {
console.log(err);
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={{ state }}>
{children}
</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
products/[handle].tsx:
import React, { useState, useEffect } from "react";
import client from "../../lib/client";
import { useCartDispatch, useCartState } from "../../context/cart";
import Link from "next/link";
import cookie from "js-cookie";
export const getStaticPaths = async () => {
const res = await client.product.fetchAll();
const paths = res.map((product: any) => {
return {
params: { handle: product.handle.toString() },
};
});
return {
paths,
fallback: false,
};
};
export const getStaticProps = async (context: any) => {
const handle = context.params.handle;
const res = await client.product.fetchByHandle(handle);
const product = JSON.stringify(res);
return {
props: {
product,
},
};
};
function Product({ product }: any) {
const { state } = useCartState();
const { setCart } = useCartDispatch();
const addToCart = async () => {
const checkoutId = state.id;
const lineItemsToAdd = [
{
variantId: product.variants[0].id,
quantity: 1,
},
];
const res = await client.checkout.addLineItems(checkoutId, lineItemsToAdd);
setCart(res);
};
product = JSON.parse(product);
return (
<div>
<div className=" flex-col text-2xl font-bold m-8 flex items-center justify-center ">
<h1>{product.title}</h1>
<button onClick={addToCart}>Add to Cart</button>
<Link href="/cart">Checkout</Link>
</div>
</div>
);
}
export default Product;
pages/cart/index.tsx:
import React, { useEffect } from "react";
import { useCartState, useCartDispatch } from "../../context/cart";
import client from "../../lib/client";
function Cart() {
const { state } = useCartState();
return (
<div>
<h1>Cart</h1>
{state.lineItems &&
state.lineItems.map((item: any) => {
return (
<div key={item.id}>
<h2>{item.title}</h2>
<p>{item.variant.title}</p>
<p>{item.quantity}</p>
</div>
);
})}
</div>
);
}
export default Cart;
I have tried using a library called js-cookie and also localStorage. I'm not sure where the problem lies or if the solutions that I've tried are wrong.
P.S.: I'm fairly new to NextJS and Typescript so go easy on the syntax. This code is for a personal project. Thanks in advance!
Answering this because I ended up coming up with a solution that works for me, at least.
Here it is:
const getCart = async () => {
try {
const checkoutId = Cookies.get("checkoutId");
let cart;
if (checkoutId) {
cart = await client.checkout.fetch(checkoutId);
} else {
cart = await client.checkout.create();
Cookies.set("checkoutId", cart.id);
}
setCart(cart);
} catch (err) {
console.log(err);
}
};
From my understanding, what this does is the following:
Check the cookies to see if one exists called "checkoutId"
If it exists, fetch the cart using that checkoutId
Otherwise, create a new cart and create a cookie using the cart.id that is returned in the response
Then, inside my individual Product page ([handle].tsx), I'm doing the following:
const addToCart = async () => {
const checkoutId = state.id;
const lineItemsToAdd = [
{
variantId: product.variants[0].id,
quantity: 1,
},
];
const res = await client.checkout.addLineItems(checkoutId, lineItemsToAdd);
console.log(res);
if (cookie.get("checkoutId") === undefined) {
cookie.set("checkoutId", res.id);
}
setCart(res);
};
Using cookies to store your object cart, as far as I know, is not a good idea. You could use localStorage, like so:
import { createContext, useContext, useEffect, useReducer } from "react";
import client from "../lib/client";
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = "SET_CART";
const initalState =
typeof localStorage !== "undefined" && localStorage.getItem("cartState")
? JSON.parse(localStorage.getItem("cartState"))
: {
lineItems: [],
totalPrice: 0,
webUrl: "",
id: "",
};
const reducer = (state, action) => {
switch (action.type) {
case SET_CART:
return { ...state, ...action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
export const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initalState);
useEffect(() => {
localStorage.set("cartState", JSON.stringify(state));
}, [state]);
useEffect(() => {
getCart();
}, []);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
const getCart = async () => {
try {
const cart = await client.checkout.create();
setCart(cart);
} catch (err) {
console.log(err);
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={{ state }}>{children}</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
I am developing a context in which through a function I can send "pokemons" to a global array, and also send the information of this array to my localstorage so that it is saved in the browser, I managed to do that and the array items are in localstorage, but every time the site refreshes, localstorage goes back to the empty array.
import React, { useEffect, useState } from "react";
import CatchContext from "./Context";
const CatchProvider = ({ children }) => {
const [pokemons, setPokemons] = useState([], () => {
const dataStorage = localStorage.getItem('pokemons');
if (dataStorage) {
return JSON.parse(dataStorage)
} else {
return [];
}
});
useEffect(() => {
localStorage.setItem('pokemons', JSON.stringify(pokemons));
}, [pokemons]);
const updatePokemons = (name) => {
const updatedPokemons = [...pokemons];
const pokemonsIndex = pokemons.indexOf(name);
if (pokemonsIndex >= 0) {
updatedPokemons.slice(pokemonsIndex, 1)
} else {
updatedPokemons.push(name)
};
setPokemons(updatedPokemons)
}
const deletePokemon = async (name) => {
await pokemons.splice(pokemons.indexOf(toString(name)))
}
return (
<CatchContext.Provider value={{ pokemons: pokemons, updatePokemons: updatePokemons, deletePokemon: deletePokemon }}>
{children}
</CatchContext.Provider>
);
}
export default CatchProvider;
The problem is that useState doesn't take two arguments.
Instead of:
const [pokemons, setPokemons] = useState([], () => {
You want:
const [pokemons, setPokemons] = useState(() => {
I think you don't need to call useEffect on initial render so you can make use of refs for this
import { useEffect, useRef } from "react";
// other code....
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) {
localStorage.setItem('pokemons', JSON.stringify(pokemons));
} else {
didMount.current = true;
}
}, [pokemons]);
I have a react component that has a html button that when clicked calls a function that adds an element to a redux reducer and then redirects to another component. The component that is redirected to needs to set state from the reducer but it won't. I know that it is being added to the array in the reducer because I wrote it as an async await and it redirects after it gets added.
This is the original component
const Posts = () => {
const dispatch = useDispatch();
const getProfile = async (member) => {
await dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
window.location.href='/member'
console.log('----------- member------------')
console.log(post)
}
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default Posts;
This is the 'socialNetworkContract' reducer
import { connect, useDispatch, useSelector } from "react-redux";
let init = {
posts:[],
post:{},
profiles:[],
profile:{},
members:[],
member:{}
}
export const socialNetworkContract = (state = init, action) => {
const { type, response } = action;
switch (type) {
case 'ADD_POST':
return {
...state,
posts: [...state.posts, response]
}
case 'SET_POST':
return {
...state,
post: response
}
case 'ADD_PROFILE':
return {
...state,
profiles: [...state.profiles, response]
}
case 'SET_PROFILE':
return {
...state,
profile: response
}
case 'ADD_MEMBER':
return {
...state,
members: [...state.members, response]
}
case 'SET_MEMBER':
return {
...state,
member: response
}
default: return state
}
};
and this is the component that the html button redirects to
const Member = () => {
const [user, setUser] = useState({})
const [profile, setProfile] = useState({});
const dispatch = useDispatch();
useEffect(async()=>{
try {
const pro = socialNetworkContract.members[0];
setUser(pro)
const p = await incidentsInstance.usersProfile(user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(user, { from: accounts[0] });
console.log(a)
setProfile(p)
} catch (e) {
console.error(e)
}
}, [])
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div class="container">
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
{p.message}
{p.replies}
</tr>})}
</div>
</div>
</div>
</div>
)
}
export default Member;
This is the error I get in the console
Error: invalid address (arg="user", coderType="address", value={})
The functions I'm calling are solidity smart contracts and the have been tested and are working and the element I'm trying to retrieve out of the array is an ethereum address.
incidentsInstance and snInstance are declared in the try statement but I took a lot of the code out to make it easier to understand.
given setUser is async, your user is still an empty object when you make your request.
you could pass pro value instead:
useEffect(async () => {
try {
const pro = socialNetworkContract.members[0];
setUser(pro)
const p = await incidentsInstance.usersProfile(pro, { from: accounts[0] });
const a = await snInstance.getUsersPosts(pro, { from: accounts[0] });
setProfile(p)
} catch (e) {
console.error(e)
}
}, [])
or break your useEffect in two pieces:
useEffect(() => {
setUser(socialNetworkContract.members[0]);
}, [])
useEffect(async () => {
if (!Object.keys(user).length) return;
try {
const p = await incidentsInstance.usersProfile(user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(user, { from: accounts[0] });
console.log(a)
setProfile(p)
} catch (e) {
console.error(e)
}
}, [user])
note: fwiw, at first sight your user state looks redundant since it's derived from a calculated value.
I am using
ReactJS 16.14.0
I have a React functional component that relies on data stored in context to render correctly, some of this data needs additional processing before display and some additional data needs fetching. the component is throwing the React has detected a change in the order of Hooks error, I have read the react docs on the rules of hooks as well as having a good look through SO but I can't work out why I get the error. I have shortened the code below to keep it brief.
const { enqueueSnackbar } = useSnackbar();
const [ mainContact, setMainContact ] = useState(undefined);
const [ mainAddress, setMainAddress ] = useState(undefined);
const [ thisLoading, setThisLoading ] = useState(true);
const { organisation, addresses, loading } = useProfileState();
useEffect(() => {
setThisLoading(true);
if(organisation && addresses && !loading) {
Promise.all([getMainContact(), getMainAddress()])
.then(() => {
setThisLoading(false);
})
.catch(() => {
console.log("Failed getting address/contact info");
setThisLoading(false);
})
}
}, [organisation, addresses, loading])
const getMainContact = () => {
return new Promise((resolve, reject) => {
apiService.getData(`/organisation/users/${organisation.mainContact}`)
.then(mainContact => {
setMainContact(mainContact);
return resolve();
})
.catch(error => {
enqueueSnackbar(error, { variant: 'error' });
return reject();
})
})
}
const getMainAddress = () => {
return new Promise((resolve, reject) => {
let mainAddress = addresses.find(addr => addr.id === organisation.mainAddress)
if(mainAddress !== undefined) {
setMainAddress(mainAddress);
return resolve();
} else {
enqueueSnackbar("Error getting main address ", { variant: 'error' });
return reject();
}
})
}
}
I just want to understand why I get this error and any potential solutions or what I am doing wrong etc. below is the full error. If I comment out the setThisLoading(false) in the .then() of the Promise.all() the error goes away but my page never displays any content because I use thisLoading to conditionally render a loading wheel or the content.
------------------------------------------------------
1. useContext useContext
2. useDebugValue useDebugValue
3. useContext useContext
4. useRef useRef
5. useRef useRef
6. useRef useRef
7. useMemo useMemo
8. useEffect useEffect
9. useEffect useEffect
10. useDebugValue useDebugValue
11. useContext useContext
12. useState useState
13. useState useState
14. useState useState
15. useState useState
16. useState useState
17. useState useState
18. useState useState
19. useContext useContext
20. useEffect useEffect
21. undefined useContext
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
I am just looking to understand why the setThisLoading(false) causes me to get this error.
Update
The useSnackbar() hook is provided by an external libary notistack
https://github.com/iamhosseindhv/notistack
Below is the code relating to useProfileState()
import React, { createContext, useContext, useReducer } from 'react';
const initialState = { loggedIn: false, loading: true, error: false }
const ProfileStateContext = createContext();
const ProfileDispatchContext = createContext();
const ProfileReducer = (state, action) => {
switch (action.type) {
case 'LOGGED_IN':
console.log("PROFILE CONTEXT - Logged in");
return { ...state, loggedIn: true }
case 'LOADED':
console.log("PROFILE CONTEXT - Data loaded");
return { ...state, loading: false, error: false }
case 'LOADING':
console.log("PROFILE CONTEXT - Data loading");
return { ...state, loading: true, error: false }
case 'ERROR':
console.log("PROFILE CONTEXT - Error");
return { ...state, loading: false, error: true }
case 'ADD_USER':
console.log("PROFILE CONTEXT - Adding user...");
return { ...state, user: { ...action.payload } }
case 'ADD_ORGANISATION':
console.log("PROFILE CONTEXT - Adding organisation...");
return { ...state, organisation: { ...action.payload } }
case 'ADD_ROLES':
console.log("PROFILE CONTEXT - Adding roles...");
return { ...state, roles: [...action.payload] }
case 'ADD_ORGANISATIONS':
console.log("PROFILE CONTEXT - Adding organisations...");
return { ...state, organisations: [...action.payload] }
case 'ADD_ADDRESSES':
console.log("PROFILE CONTEXT - Adding addresses...");
return { ...state, addresses: [...action.payload] }
case 'LOGOUT':
console.log("PROFILE CONTEXT - Removing context data...");
return initialState;
default:
console.error(`Unhandled action dispatched to user reducer, action type was: ${action.type}`);
return state;
}
}
const ProfileProvider = ({ children }) => {
const [state, dispatch] = useReducer(ProfileReducer, initialState)
return (
<ProfileStateContext.Provider value={state}>
<ProfileDispatchContext.Provider value={dispatch}>
{children}
</ProfileDispatchContext.Provider>
</ProfileStateContext.Provider>
);
};
const useProfileState = () => {
const context = useContext(ProfileStateContext);
if (context === undefined) {
throw new Error('useProfileState must be used within a ProfileContextProvider')
}
return context;
};
const useProfileDispatch = () => {
const context = useContext(ProfileDispatchContext);
if (context === undefined) {
throw new Error('useProfileDispatch must be used within a ProfileContextProvider')
}
return context;
};
export {
ProfileProvider,
useProfileDispatch,
useProfileState
}
Update 2
I have also tried chaining the promises and adding a dummy cleanup func as suggested, I still get the same error.
useEffect(() => {
setThisLoading(true);
if(organisation && addresses && !loading) {
getMainContact()
.then(() => {
getMainAddress()
.then(() => {
getBillingContact()
.then(() => {
getBillingAddress()
.then(() => {
setThisLoading(false);
})
})
})
})
}
return () => {};
}, [organisation, addresses, loading])
I found the problem to be in a completely different component to the one the error was indicating towards. The setThisLoading(false) was a red herring as this just allowed the problem component to render therefore giving the error. The way I found this out was via Chrome’s console, I usually work in Firefox as this is my browser of choice but this time Chrome came to the rescue as it gave more information as to where the error was originating from.
The application I am building has the concept of user roles, allowing/denying users to perform certain tasks. I wrote some functions to assist in the disabling of buttons and/or not showing content based on the role the logged in user had. This is where the problem lies.
Old RoleCheck.js
import React from 'react';
import { useProfileState } from '../../context/ProfileContext';
//0 - No permission
//1 - Read only
//2 - Read/Write
const _roleCheck = (realm, permission) => {
const { organisation, organisations, roles, loading } = useProfileState();
if(!loading) {
//Get the RoleID for the current account
const currentOrganisation = organisations.find(org => org.id === organisation.id);
//Get the Role object by RoleID
const currentRole = roles.find(role => role.id === currentOrganisation.roleId);
if(currentRole[realm] === permission) {
return true;
} else {
return false;
}
}
};
export const roleCheck = (realm, permission) => {
//Reversed boolean for button disabling
if(_roleCheck(realm, permission)) {
return false;
} else {
return true;
}
};
export const RoleCheck = ({ children, realm, permission }) => {
if(_roleCheck(realm, permission)) {
return (
<React.Fragment>
{ children }
</React.Fragment>
);
} else {
return (
<React.Fragment />
);
}
};
Usage of old RoleCheck.js
import { roleCheck } from '../Utils/RoleCheck';
...
<Button variant="outlined" disabled={roleCheck("organisation", 2)} color="primary">
edit organisation
</Button>
New RoleCheck.js
import React from 'react';
import { useProfileState } from '../../context/ProfileContext';
//0 - No permission
//1 - Read only
//2 - Read/Write
export const useRoleCheck = () => {
const { organisation, organisations, roles, loading } = useProfileState();
const _roleCheck = (realm, permission) => {
if(!loading) {
//Get the RoleID for the current account
const currentOrganisation = organisations.find(org => org.id === organisation.id);
//Get the Role object by RoleID
const currentRole = roles.find(role => role.id === currentOrganisation.roleId);
if(currentRole[realm] === permission) {
return true;
} else {
return false;
}
}
};
const RoleCheckWrapper = ({ children, realm, permission }) => {
if(_roleCheck(realm, permission)) {
return (
<React.Fragment>
{ children }
</React.Fragment>
);
} else {
return (
<React.Fragment />
);
}
};
const roleCheck = (realm, permission) => {
//Reversed boolean for button disabling
if(_roleCheck(realm, permission)) {
return false;
} else {
return true;
}
};
return {
roleCheck: roleCheck,
RoleCheckWrapper: RoleCheckWrapper
}
}
Usage of new RoleCheck.js
import { useRoleCheck } from '../Utils/RoleCheck';
...
const RequiresRoleCheck = () => {
const rc = useRoleCheck();
return (
<Button variant="outlined" disabled={rc.roleCheck("organisation", 2)} color="primary">
edit organisation
</Button>
)
}
By turning my Role Check functions into a hook I am able to call hooks inside it and I am able to call the useRoleCheck() hook at the top level of components that need to use it therefore not breaking the rules of hooks!
I have a situation where i can successfully dispatch my states with reducers and i can render it in my component
Here the relevant code
in my action/index.js
export const receivedLeaguesList = json => ({
type: RECEIVE_LEAGUES_LIST,
json: json
});
export function fetchLeaguesList() {
return function(dispatch) {
dispatch(requestLeaguesList());
return axios
.get("https://www.api-football.com/demo/v2/leagues/")
.then(res => {
let leagues = res.data.api.leagues;
dispatch(receivedLeaguesList(leagues));
})
.catch(e => {
console.log(e);
});
}
}
my reducers/index.js
import { REQUEST_LEAGUES_LIST, RECEIVE_LEAGUES_LIST } from "../actions";
const initialState = {
leaguesList: [],
isLeagueListLoading: false
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case REQUEST_LEAGUES_LIST:
return { ...state, isLeagueListLoading: true };
case RECEIVE_LEAGUES_LIST:
return { ...state, leaguesList: action.json, isLeagueListLoading: false };
default:
return state;
}
};
in my component component/Leagues.js
let Leagues = ({ leaguesList, loading, getList }) => {
useEffect(() => {
getList();
}, [getList]);
const [itemsLeagues] = useState([leaguesList]);
console.log("league list", itemsLeagues);
const mapDispatchToProps = {
getList: fetchLeaguesList
};
I have reproduced the demo here => https://codesandbox.io/s/select-demo-71u7h?
I can render my leaguesList states in my component doing the map, but why when
const [itemsLeagues] = useState([leaguesList]);
console.log("league list", itemsLeagues);
returns an empty array ?
See the image
You're setting useState's init value wrong:
const [itemsLeagues] = useState(leaguesList);
instead of
const [itemsLeagues] = useState([leaguesList]);
The return value of useState isn't the value itself, but the array of value and mutator:
const [value, setValue] = useState([42, 43])
// here's value equals [42, 43]
So if you were trying to destructure the wrapping array you passed to useState(), you should use it like this (though you don't need it):
const [[itemsLeagues]] = useState([leaguesList]);