React Redux state not changing after calling useDispatch() [SOLVED] - javascript

SOLVED
I'm trying to use the newer version of react redux v8.0.2 within my web-based game to keep state within my application without having to pass states between navigation calls.
Unfortunately I'm running into an issue where the player state gets initialized for the first time, but the final state is not being updated after calling the useDispatch() method. I've tried looking everywhere online, but none of the solutions that are currently out there have actually solved my issue.
I even force my functional component to rerender, and that still just returns the initial state of my player instead of the updated one that I'm expecting. Can anyone help me figure out what I'm missing here. It's got to be something small that isn't talked about on the redux forms/docs. TIA!
playerSlice.js
import {createSlice} from '#reduxjs/toolkit';
export const playerSlice = createSlice({
name: 'player',
initialState: {
address: '',
cp: 0,
created: '',
faction: 0,
faction_selected: false,
games_lost: 0,
games_won: 0,
online: false,
selected_char: 0,
selected: {
combatType: '',
lvl: 0,
mgc: 0,
str: 0,
rng: 0,
def: 0
},
time_played: 0,
tokens: 10000,
total_cp: 0,
total_earned: 0,
user_name: ""
},
reducers: {
setInit: (state,action) => {
state = action.payload;
},
setCP: (state,action) => {
state.cp += action.payload;
},
setFaction: (state,action) => {
state.faction = action.payload;
},
setGamesLost: (state,action) => {
state.games_lost = action.payload;
},
setGamesWon: (state,action) => {
state.games_won = action.payload;
},
setPlayerState: (state,action) => {
state = {
...state,
...action.payload
}
}
}
});
export const {setInit, setCP, setFaction, setGamesLost, setGamesWon, setPlayerState} = playerSlice.actions;
export const selectPlayer = (state) => state.player;
export default playerSlice.reducer;
index.js
import {configureStore} from '#reduxjs/toolkit';
import playerReducer from '../store/playerSlice';
export default configureStore({
reducer: {
player: playerReducer,
},
})
Selection.js
import Card from './Card';
import React, {useState, useEffect} from 'react';
import '../stylesheet/Selection.css';
import Logo from '../assets/degen age title GNW skull.png';
import KnightTitle from '../assets/knights title.png';
import GoblinTitle from '../assets/goblins title.png';
import WizardTitle from '../assets/wizards title.png';
import ElfTitle from '../assets/elves title.png';
import SorcererShield from '../assets/sorcerers shield item.jpg';
import Weaken from '../assets/weaken item img.jpg';
import Barrage from '../assets/barrage item img.jpg';
import Berserk from '../assets/berserk item img.jpg';
import {db} from '../firebase/firestore';
import {addDoc,collection, serverTimestamp} from 'firebase/firestore';
import { useNavigate, useLocation } from 'react-router-dom';
import {CHAR_RACES} from '../constants';
import {useSelector, useDispatch} from 'react-redux';
import {setInit, selectPlayer} from '../store/playerSlice';
const SCREEN_DELAY = 4000; // delay in ms
const Selection = () => {
const [initScreen, setInitScreen] = useState(true);
const player = useSelector(selectPlayer);
const dispatch = useDispatch();
const navigate = useNavigate();
const [ready,setReady] = useState(false);
const {state} = useLocation();
useEffect(() => {
let mounted = true;
if(mounted){
setTimeout(() => {
setInitScreen(false);
},SCREEN_DELAY);
}
return () => {
mounted = false;
}
},[]);
const handleFactionSelect = async (_faction) => {
// add new player to db
const ref = collection(db, 'players');
const playerData = {
address: state.address,
cp: 0,
created: serverTimestamp(),
faction: _faction,
faction_selected: true,
games_lost: 0,
games_won: 0,
online: true,
selected_char: 0,
selected: {
combatType: 'MELEE',
lvl: 200,
mgc: 10,
str: 59,
rng: 30,
def: 101
},
time_played: 0,
tokens: 10000,
total_cp: 0,
total_earned: 0,
user_name: "someUser393900"
}
dispatch(setInit({
...playerData,
created: new Date().getTime(),
faction: _faction
}))
console.log({player});
// set in redux as well****
// addDoc(ref,playerData).then(res => {
// if(res.id){
// const _faction = CHAR_RACES[playerData.faction];
// dispatch(setInit({
// ...playerData,
// created: new Date().getTime(),
// faction: _faction
// }))
// // navigate('/play',{
// // state: {
// // player: {
// // ...playerData,
// // faction: _faction
// // }
// // }
// // });
// navigate('/play');
// }
// }).catch(error => {
// console.error(error);
// })
}
return (
<div className='select-main'>
{!initScreen ? <div id="main-select" className='fade-in-slow2 select-wrapper'>
<h1 className='text-center'>CHOOSE YOUR SIDE</h1>
<div className='select-cards'>
<Card cardStyle="f1" ability={SorcererShield} desc="Sorcerers Shield" title={WizardTitle} name={1} onClick={handleFactionSelect} />
<Card cardStyle="f3" ability={Berserk} desc="Berserk" title={KnightTitle} name={2} onClick={handleFactionSelect} />
<Card cardStyle="f2" ability={Barrage} desc="Barrage" title={ElfTitle} name={0} onClick={handleFactionSelect} />
<Card cardStyle="f4" ability={Weaken} desc="Weaken" title={GoblinTitle} name={3} onClick={handleFactionSelect} />
</div>
</div> :
<div className='fade-in-slow sub-select-wrapper flex-just-center'>
<div className='cracked'></div>
</div>
}
</div>
)
}
export default Selection;
As you can see I'm attempting to call the setInit reducer from my redux store and then logging the new state after that. I also know that trying to log the state directly after doesn't always reflect the most recent data, but I've tried by adding a state change to my component and log the player state afterwards and I just get the same data back again. Nothing changes.

setInit: (state,action) => {
state = action.payload;
},
This doesn't do anything, it simply replaces the local variable state but Redux cannot possibly see that. You should either mutate the state, or return a new state. In your case, returning action.payload should work.
See https://redux-toolkit.js.org/usage/immer-reducers#resetting-and-replacing-state

Related

State update not reflecting in custom hook

I am using react-redux to store a state modeData which is an array of objects. Using react-hotkeys-hook, when the user presses ctrl+l, a function runs which updates the state. In that same function, when I console.log the same state, it does not reflect the changes.
Here's the code I'm using:
import { useSelector, useDispatch } from "react-redux";
import { modeDataIncrement } from "./redux/modeSlice";
import { useHotkeys } from "react-hotkeys-hook";
import { useEffect } from "react";
//..
const modeData = useSelector((state) => state.mode.modeData); //array of objects.
const handler = (id) => {
const isActive = modeData.find((x) => x.id === id).active;
console.log(modeData); // does not show the updated state
dispatch(modeDataIncrement(id));
};
useHotkeys("ctrl+l", () => handler("Clone"));
useEffect(() => {
console.log(modeData); //working fine!
}, [modeData]);
modeSlice.js:
import { createSlice } from "#reduxjs/toolkit";
export const modeSlice = createSlice({
name: "mode",
initialState: {
modeData: [{ id: "Clone", active: false }],
},
reducers: {
modeDataIncrement: (state, action) => {
state.modeData.find(
(e) => e.id === action.payload && (e.active = !e.active)
);
},
},
});
export const { modeDataIncrement } = modeSlice.actions;
export default modeSlice.reducer;
Any thoughts on what I'm doing wrong?
Thanks!
Your code needs to use the latest version of modeData, but based on the source code of useHotkeys, it will memoize the function, and not update it automatically. So you're always using the version of the callback that existed on the first render.
To fix this, you need to pass a dependency array in to useHotkeys, so it can break the memoization:
const handler = (id) => {
const isActive = modeData.find((x) => x.id === id).active;
console.log(modeData);
dispatch(modeDataIncrement(id));
};
useHotkeys("ctrl+l", () => handler("Clone"), [modeData]);

Adyencheckout is not a constructor - NextJS

I'm trying to implement the Adyen dropin payment UI using NextJS but I'm having trouble initializing the Adyen dropin component.
I'm need to dynamically import Adyen web or I get the error window is not defined however, after reading through the NextJS docs, dynamic import creates a component which I can't figure out how to use as a constructor.
I tried the code below but receive the error TypeError: AdyenCheckout is not a constructor
I'm new to NextJS and am at a total loss as to how I should import and initialize Adyen.
Can anyone point me in the right direction?
import Head from 'next/head';
import { useRef, useEffect, useState } from 'react';
import {callServer, handleSubmission} from '../util/serverHelpers';
//dynamic import below. Imports as a component
//import dynamic from 'next/dynamic';
//const AdyenCheckout = dynamic(() => import('#adyen/adyen-web'), {ssr: false});
import '#adyen/adyen-web/dist/adyen.css';
export default function Dropin(){
const dropinContainer = useRef(null);
const [paymentMethods, setPaymentMethods] = useState();
//const [dropinHolder, setDropinHolder] = useState();
//Get payment methods after page render
useEffect( async () => {
const response = await callServer(`${process.env.BASE_URL}/api/getPaymentMethods`);
setPaymentMethods(prev => prev = response);
},[]);
//Adyen config object to be passed to AdyenCheckout
const configuration = {
paymentMethodsResponse: paymentMethods,
clientKey: process.env.CLIENT_KEY,
locale: "en_AU",
environment: "test",
paymentMethodsConfiguration: {
card: {
showPayButton: true,
hasHolderName: true,
holderNameRequired: true,
name: "Credit or debit card",
amount: {
value: 2000,
currency: "AUD"
}
}
},
onSubmit: (state, component) => {
if (state.isValid) {
handleSubmission(state, component, "/api/initiatePayment");
}
},
onAdditionalDetails: (state, component) => {
handleSubmission(state, component, "/api/submitAdditionalDetails");
},
};
//const checkout = new AdyenCheckout(configuration);
const AdyenCheckout = import('#adyen/adyen-web').default;
const adyenCheckout = new AdyenCheckout(configuration);
const dropin = adyenCheckout.create('dropin').mount(dropinContainer.current);
return (
<div>
<Head>
<title>Dropin</title>
</Head>
<div ref={dropin}></div>
</div>
)
}
I was able to resolve the issue by importing the module using the default value inside an async function nested in the useEffect function.
import Head from 'next/head';
import { useRef, useEffect, useState } from 'react';
import {callServer, handleSubmission} from '../util/serverHelpers';
import '#adyen/adyen-web/dist/adyen.css';
export default function Dropin(){
const dropinContainer = useRef();
const [paymentMethods, setPaymentMethods] = useState({});
useEffect(() => {
const init = async () => {
const response = await callServer(`${process.env.BASE_URL}/api/getPaymentMethods`)
.then(setPaymentMethods(response));
console.log(paymentMethods);
const configuration = {
paymentMethodsResponse: paymentMethods,
clientKey: process.env.CLIENT_KEY,
locale: "en_AU",
environment: "test",
paymentMethodsConfiguration: {
card: {
showPayButton: true,
hasHolderName: true,
holderNameRequired: true,
name: "Credit or debit card",
amount: {
value: 2000,
currency: "AUD"
}
}
},
onSubmit: (state, component) => {
if (state.isValid) {
handleSubmission(state, component, "/api/initiatePayment");
}
},
onAdditionalDetails: (state, component) => {
handleSubmission(state, component, "/api/submitAdditionalDetails");
},
};
console.log(configuration.paymentMethodsResponse);
const AdyenCheckout = (await import('#adyen/adyen-web')).default;
const checkout = new AdyenCheckout(configuration);
checkout.create('dropin').mount(dropinContainer.current);
}
init();
},[]);
return (
<div>
<Head>
<title>Dropin</title>
</Head>
<div ref={dropinContainer}></div>
</div>
)
}

React Hooks/Context & Elastictic UI. Problem with fetched data (REST) in function Component

I'm quite new to React Hooks/Context so I'd appreciate some help. Please don' t jump on me with your sharp teeth. I Checked other solutions and some ways i've done this before but can't seem to get it here with the 'pick from the list' way.
SUMMARY
I need to get the municipios list of names inside of my const 'allMunicipios'(array of objects) inside of my Search.js and then display a card with some data from the chosen municipio.
TASK
Get the data from eltiempo-net REST API.
Use Combobox async element from Elastic UI to choose from list of municipios.
Display Card (from elastic UI too) with some info of chosen municipio.
It has to be done with function components / hooks. No classes.
I'd please appreciate any help.
WHAT I'VE DONE
I've created my reducer, context and types files in a context folder to fecth all data with those and then access data from the component.
I've created my Search.js file. Then imported Search.js in App.js.
I've accesed the REST API and now have it in my Search.js
PROBLEM
Somehow I'm not beeing able to iterate through the data i got.
Basically i need to push the municipios.NOMBRE from api to the array const allMunicipios in my search.js component. But when i console log it it gives me undefined. Can;t figure out why.
I'll share down here the relevant code/components. Thanks a lot for whoever takes the time.
municipiosReducer.js
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
export default (state, action) => {
switch (action.type) {
case SEARCH_MUNICIPIOS:
return {
...state,
municipios: action.payload,
};
case GET_MUNICIPIO:
return {
...state,
municipio: action.payload,
};
case CLEAR_MUNICIPIOS:
return {
...state,
municipios: [],
};
case GET_WEATHER: {
return {
...state,
weather: action.payload,
};
}
default:
return state;
}
};
municipiosContext.js
import { createContext } from "react";
const municipiosContext = createContext();
export default municipiosContext;
MunicipiosState.js
import React, { createContext, useReducer, Component } from "react";
import axios from "axios";
import MunicipiosContext from "./municipiosContext";
import MunicipiosReducer from "./municipiosReducer";
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
const MunicipiosState = (props) => {
const initialState = {
municipios: [],
municipio: {},
};
const [state, dispatch] = useReducer(MunicipiosReducer, initialState);
//Search municipios
//In arrow functions 'async' goes before the parameter.
const searchMunicipios = async () => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios`
// 08 means barcelona province. This should give me the list of all its municipios
);
dispatch({
type: SEARCH_MUNICIPIOS,
payload: res.data.municipios,
});
};
//Get Municipio
const getMunicipio = async (municipio) => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios/${municipio.CODIGOINE}`
//CODIGOINE is in this REST API kind of the ID for each municipio.
//I intent to use this later to get the weather conditions from each municipio.
);
dispatch({ type: GET_MUNICIPIO, payload: res.municipio });
};
const dataMunicipiosArray = [searchMunicipios];
//Clear Municipios
const clearMunicipios = () => {
dispatch({ type: CLEAR_MUNICIPIOS });
};
return (
<MunicipiosContext.Provider
value={{
municipios: state.municipios,
municipio: state.municipio,
searchMunicipios,
getMunicipio,
clearMunicipios,
dataMunicipiosArray,
}}
>
{props.children}
</MunicipiosContext.Provider>
);
};
export default MunicipiosState;
Search.js
import "#elastic/eui/dist/eui_theme_light.css";
import "#babel/polyfill";
import MunicipiosContext from "../contexts/municipiosContext";
import MunicipiosState from "../contexts/MunicipiosState";
import { EuiComboBox, EuiText } from "#elastic/eui";
import React, { useState, useEffect, useCallback, useContext } from "react";
const Search = () => {
const municipiosContext = useContext(MunicipiosContext);
const { searchMunicipios, municipios } = MunicipiosState;
useEffect(() => {
return municipiosContext.searchMunicipios();
}, []);
const municipiosFromContext = municipiosContext.municipios;
const bringOneMunicipio = municipiosContext.municipios[0];
let municipiosNames = municipiosFromContext.map((municipio) => {
return { label: `${municipio.NOMBRE}` };
});
console.log(`municipiosFromContext`, municipiosFromContext);
console.log(`const bringOneMunicipio:`, bringOneMunicipio);
console.log(`municipiosNames:`, municipiosNames);
const allMunicipios = [
{ label: "santcugat" },
{ label: "BARCELONETA" },
{ label: "BARCE" },
];
const [selectedOptions, setSelected] = useState([]);
const [isLoading, setLoading] = useState(false);
const [options, setOptions] = useState([]);
let searchTimeout;
const onChange = (selectedOptions) => {
setSelected(selectedOptions);
};
// combo-box
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, []);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);
return (
<div>
<EuiComboBox
placeholder="Search asynchronously"
async
options={options}
selectedOptions={selectedOptions}
isLoading={isLoading}
onChange={onChange}
onSearchChange={onSearchChange}
/>
<button>Lista de municipios</button>
</div>
);
};
export default Search;
also the
Home.js
import React, { useState } from "react";
import { EuiComboBox, EuiText } from "#elastic/eui";
// import { DisplayToggles } from "../form_controls/display_toggles";
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import Search from "./Search";
import MunicipioCard from "./MunicipioCard";
const Home = () => {
return (
<div>
<EuiText grow={false}>
<h1>Clima en la provincia de Barcelona</h1>
<h2>Por favor seleccione un municipio</h2>
</EuiText>
<Search />
<MunicipioCard />
</div>
);
};
export default Home;
App.js
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import { EuiText } from "#elastic/eui";
import React from "react";
import Home from "./components/Home";
import MunicipiosState from "./contexts/MunicipiosState";
import "./App.css";
function App() {
return (
<MunicipiosState>
<div className="App">
<EuiText>
<h1>App Component h1</h1>
</EuiText>
<Home />
</div>
</MunicipiosState>
);
}
export default App;
You are using forEach and assigning the returned value to a variable, however forEach doesn't return anything. You should instead use map
let municipiosNames = municipiosFromContext.map((municipio) => {
return `label: ${municipio.NOMBRE}`;
});
As per your comment:
you data is loaded asynchronously, so it won't be available on first render and since functional components depend on closures, you onSearchChange function takes the value from the closure at the time of creation and even if you have a setTimeout within it the updated value won't reflect
The solution here is to add municipiosFromContext as a dependency to useEffect
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, [municipiosFromContext]);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);

Redux - ReactJS app does not rerender (although JSON.parse for new object)

I have a ReactJS app with filters and use a RESET function for resetting those filters.
What I also use: Redux, Redux Persist and React-router-dom.
If I have a look at the Redux Devtools, it seems to work. But the App does not rerender correctly, a refresh (f5) is necessary.
What I want to achieve: override the configuredFilters object with the initialState object.
This is my root reducer:
const rootReducer = (state = initialState, action) => {
let newState = state;
if (action.type === 'RESET_FILTERS') {
storage.removeItem('persist:configuredFilters');
// eslint-disable-next-line no-param-reassign
newState = { ...newState,
configuredFilters: JSON.parse(JSON.stringify(initialState.configuredFilters))
};
}
return appReducer(newState, action);
};
Here is the diff (I configured two countries before):
Here is the object (initial status if the page is loaded):
The components are created with this component:
/* eslint-disable max-len */
import React from 'react';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { Form, Select } from 'antd';
import PropTypes from 'prop-types';
import RenderTag from './RenderTag';
import * as Action from '../../store/configuredFilters/actions';
const propTypes = {
data: PropTypes.shape({
name: PropTypes.string.isRequired,
label: PropTypes.string,
placeholder: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({})),
}).isRequired,
};
const { Option } = Select;
const useData = () => {
const dataFromRdx = useSelector(
(state) => ({
configuredFilters: state.configuredFilters,
}),
shallowEqual
);
return { dataFromRdx };
};
const FixedListSelect = ({
data: {
name, label, placeholder, options,
},
}) => {
const { dataFromRdx } = useData();
const {
configuredFilters: {
data: {
search: searchTerm,
},
},
} = dataFromRdx;
const dispatch = useDispatch();
const dispatchFns = {
setConfiguredFilters: (key, value) => {
dispatch(Action.setConfiguredFilters(key, value));
},
};
const setRdxVal = (id, currVal) => {
dispatchFns.setConfiguredFilters(id, currVal);
};
const isFullTextSearchMode = (searchTerm && searchTerm.length);
return (
<Form.Item
name={name}
label={label}
fieldKey={name}
>
<Select
allowClear
disabled={isFullTextSearchMode}
showSearch
tagRender={RenderTag}
mode="multiple"
placeholder={placeholder}
optionFilterProp="children"
filterOption={(input, option) => option.children.toLowerCase().includes(input.toLowerCase())}
onChange={(currVal) => { setRdxVal(name, currVal); }}
>
{(options || []).map((el) => <Option data-filterid={el.val} key={el.val} value={el.val}>{el.label}</Option>)}
</Select>
</Form.Item>
);
};
FixedListSelect.propTypes = propTypes;
export default FixedListSelect;
A call of this component:
<FixedListSelect data={{
name: 'companies',
label: t('companies'),
placeholder: t('companies-placeholder'),
options: companies,
}}
/>
Can someone help or at least give a hint?
In ReactJS you can not mutate the state directly. It is the same in Redux. You have to copy the existing info. Redux compares the current tree with new information if founds the difference then it updates the store and renders the new result. If mutating directly you will always refresh to have it working.
More info
Immutable Update Patterns
Please clone last data.
JSON.parse(JSON.stringify(data));

I am getting this error in the console : Cannot read property 'some' of undefined

TypeError: Cannot read property '' of undefined ı have no idea why ı am getting this error while I do check the code below everything seems fine :( trying to learn the way how react works :)
So what is the purpose of this since all the properties I wrap on contextprovider suchas contacts loading and the functions I need
import React, { useState, useContext } from 'react'
import ContactContext from '../context/contactContext'
export default function ContactForm() {
const name = useFormInput('')
const email = useFormInput('')
const contactContext = useContext(ContactContext)
const { addContact } = contactContext
const onSubmit = () => {
addContact(name.value, email.value)
name.onReset()
email.onReset()
}
return (
SOME HTML CODE HERE
)
}
//contactState.js
import React, { useReducer } from 'react'
import _ from 'lodash'
import ContactContext from './contactContext'
import ContactReducer from './contactReducer'
const ContactState = props => {
const initialState = {
contacts: [
{
id: '098',
name: 'Diana Prince',
email: 'diana#us.army.mil'
}
],
loading: false,
error: null
}
const [state, dispatch] = useReducer(ContactReducer, initialState)
const [contacts, loading] = state
const addContact = (name, email) => {
dispatch({
type: 'ADD_CONTACT',
payload: { id: _.uniqueId(10), name, email }
})
}
const delContact = id => {
dispatch({
type: 'DEL_CONTACT',
payload: id
})
}
return (
<ContactContext.Provider
value={{
contacts,
loading,
addContact,
delContact
}}
>
{props.children}
</ContactContext.Provider>
)
}
export default ContactState
//contactReducer.js
export default (state, action) => {
switch (action.type) {
case 'ADD_CONTACT':
return {
contacts: [...state, action.payload]
}
case 'DEL_CONTACT':
return {
contacts: state.contacts.filter(
contact => contact.id !== action.payload
)
}
case 'START':
return {
loading: true
}
case 'COMPLETE':
return {
loading: false
}
default:
throw new Error()
}
}
//contactContext.js
import { createContext } from 'react'
const contactContext = createContext()
export default contactContext
In your reducer, when adding a contact, you're spreading the wrong state key. This should fix it:
case 'ADD_CONTACT':
return {
contacts: [...state.contacts, action.payload]
}
I can't see where you are using ContactState in your app. If you don't use it and render your ContactForm component with it then you can't reach any context value. You should render it as:
<ContactState>
<ContactForm />
</ContactState>
in a suitable place in your app. Also, you can't get contacts and loading like that:
const [ contacts, loading ] = state;
state is not an array, it is an object here. You should use:
const { contacts, loading } = state
You can find a simplified version of your code below. I removed/changed some parts in order to run it as much as possible. You should fix your reducer as #Asaf David mentioned in their answer, but this is not the main problem here. After fixing the context issue, you can try to fix your reducer.
About your questions, if you try to understand how React works by looking at this example you can easily get confused. Because Context is an advanced concept (at least for the beginners). Also, the code uses useReducer with Context and this makes the things more complicated. If your intent is to understand the React itself then start with the beginner guide.
By using Context we can pass the data top-down to the deepest components. But, in order to use that data those components should be rendered as children of the context provider.
In your code, you are doing this in ContactState but you never use it. Also, in that component, you are defining a state with useReducer and feed your context with this state by value.
Finally, in your ContactForm component, you are using useContext hook to get the context data. In your current code since you don't render this component in a provider, contactContext is undefined and you are getting the error. You can't get addContact from undefined.
In my example, I'm retrieving the contacts to show something. Again, I've changed/removed some parts from your code.
const { createContext, useContext, useReducer } = React;
const ContactContext = createContext();
function ContactForm() {
// Changed those values
const name = "";
const email = "";
const contactContext = useContext(ContactContext);
// changed addContact -> contacts
const { contacts } = contactContext;
const onSubmit = () => {
addContact(name.value, email.value);
name.onReset();
email.onReset();
};
// added some data to render
return <div>{contacts[0].name}</div>;
}
function ContactReducer(state, action) {
switch (action.type) {
case "ADD_CONTACT":
return {
contacts: [...state, action.payload]
};
case "DEL_CONTACT":
return {
contacts: state.contacts.filter(
contact => contact.id !== action.payload
)
};
case "START":
return {
loading: true
};
case "COMPLETE":
return {
loading: false
};
default:
throw new Error();
}
}
const ContactState = props => {
const initialState = {
contacts: [
{
id: "098",
name: "Diana Prince",
email: "diana#us.army.mil"
}
],
loading: false,
error: null
};
const [state, dispatch] = useReducer(ContactReducer, initialState);
const { contacts, loading } = state;
const addContact = (name, email) => {
dispatch({
type: "ADD_CONTACT",
// removed lodash part, added a static id
payload: { id: 1, name, email }
});
};
const delContact = id => {
dispatch({
type: "DEL_CONTACT",
payload: id
});
};
return (
<ContactContext.Provider
value={{
contacts,
loading,
addContact,
delContact
}}
>
{props.children}
</ContactContext.Provider>
);
};
// added the relevant render part
ReactDOM.render(
<ContactState>
<ContactForm />
</ContactState>,
document.getElementById("root")
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />

Categories