NextJS: Context API for storing and passing values - reactjs

I am building this project on NextJS where I want to build a shopping cart. For that, I decided to use useContext hook.
So basically what I did is pretty basic. Create a ShoppingCartContext, add a addToCart function which will push the data to store. But when I press the add to cart button it shows me that,
store.push is not a function.
edit: When I press the button first time, nothing happens. When I press it the 2nd time it gives the mentioned error.
I think I did some basic stuff wrong in the code which is why I am getting this error.
My context code
import {
createContext,
useCallback,
useContext,
useState,
useEffect,
} from "react";
const ShoppingCartContext = createContext();
export function ShoppingCartContextProvider({ children }) {
const [store, setStore] = useState([]);
return (
<ShoppingCartContext.Provider
value={{
store,
setStore,
}}
>
{children}
</ShoppingCartContext.Provider>
);
}
export function useShoppingCartContext() {
const { store, setStore } = useContext(ShoppingCartContext);
//function to push data to array
const addToCart = (product) => {
setStore(store.push(product));
};
return {
store,
setStore,
addToCart,
};
}
Where I called the button component to add data
import { useRouter } from "next/router";
import { useState } from "react";
import { useShoppingCartContext } from "../../context/CartContext";
const SingleProduct = ({ product, categories }) => {
const { store, setStore, addToCart } = useShoppingCartContext();
const router = useRouter();
const breadcrumb = router.components["/shop/[[...category]]"].resolvedAs;
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<>
<Button
onClick={() => {
addToCart(product); // product is basically an object which holds all the info of the single product
}}
>
Add to Cart
</Button>
</>
)
}
my _app.js file
import "../styles/globals.css";
import { ChakraProvider } from "#chakra-ui/react";
import { ShoppingCartContextProvider } from "../context/CartContext";
function MyApp({ Component, pageProps }) {
return (
<ChakraProvider>
<ShoppingCartContextProvider>
<Component {...pageProps} />
</ShoppingCartContextProvider>
</ChakraProvider>
);
}
export default MyApp;
please take a look at this.

For updating state which is array you need to do
setStore(store => [...store, product])
//OR
// make clone of store and then setStore
temp=[...store]
temp.push(product)
setStore(temp)
EXPLANATION OF CURRENT BEHAVIOUR
In your case it's working 1st time as you have initialized store as [] empty array.
And because it's an array it has .push() method.
But [].push(val) will not return an array it will return the length of array.
And therefore after 1st time store get's the value of numeric datatype which does not has .push() method.
Below you can see the result of .push() your self.
arr=[1,2,3] // length 3
console.log(`initial array`)
console.log(arr)
console.log(`spread operator + new val`)
console.log([...arr,5]) // spread operator + new val
console.log(`.push()`)
console.log(arr.push(5),"length=",arr.length) // .push() this will return length of the arr

Related

Unable to run the function from the context

I have a context, I import it into my functional component:
import { TaskContexts } from "../../../contexts";
The context stores data and functions.
The data comes from the context and is displayed on the site.
const {
editTodo,
setEditID,
toggleTodoCompletion,
editID,
editTodoHandler,
removeTodo,
state,
text,
isEditError,
} = useContext(TaskContexts);
But!
<button onClick={() => editTodo(todo.id)}>
<img src={editIcon} alt="edit button"></img>
</button>
When I try to call the editTodo function, It fails with the following error:
Uncaught TypeError: editTodo is not a function
How to fix this error?
UPD.
Full component code
import React, { useState } from 'react';
import ACTION_TYPES from '../ToDo/reducer/actionTypes';
import RenderedTable from './RenderedTable';
import styles from './TaskList.module.scss';
import allIcon from '../../icons/all.svg';
import completedIcon from '../../icons/completed.svg';
import notCompletedIcon from '../../icons/notCompleted.svg';
import mona from '../../icons/mona.gif';
import { TODO_TASK_CHEMA } from '../../utils/validationSchemas';
import { TaskContexts } from '../../contexts';
const TaskList = props => {
const {
reducerData: [state, dispatch],
} = props;
const [editID, setEditID] = useState(null);
const [editText, setEditText] = useState(null);
const [isEditError, setIsEditError] = useState(false);
const [mode, setMode] = useState('All');
const removeTodo = id => {
dispatch({ type: ACTION_TYPES.REMOVE, id });
};
const toggleTodoCompletion = id => {
dispatch({ type: ACTION_TYPES.TOGGLE, id });
};
const editTodo = id => {
const text = editText.trim();
try {
TODO_TASK_CHEMA.validateSync({ text });
} catch (e) {
setIsEditError(true);
throw new Error(e);
}
setIsEditError(false);
setEditID(null);
dispatch({ type: ACTION_TYPES.EDIT, id, text });
setEditText(null);
};
const editTodoHandler = ({ target: { value } }) => {
setEditText(value);
};
const contextsValues = {
editID,
setEditID,
editText,
setEditText,
isEditError,
setIsEditError,
mode,
setMode,
state
};
return (
<TaskContexts.Provider value={contextsValues}>
<div className={styles.container}>
{state.todos.length === 0 ? (
<div>
<h2 className={styles.noTask}>No tasks =)</h2>
<img src={mona} alt='mona gif' />
</div>
) : (
<>
<button
className={styles.section}
onClick={() => {
setMode('All');
}}
>
<img src={allIcon} alt='all button' />- All
</button>
<button
className={styles.section}
onClick={() => {
setMode('Completed');
}}
>
<img src={completedIcon} alt='completed button' />- Completed
</button>
<button
className={styles.section}
onClick={() => {
setMode('NotCompleted');
}}
>
<img src={notCompletedIcon} alt='not completed button' />- Not
completed
</button>
<RenderedTable
editTodo={editTodo}
setEditID={setEditID}
toggleTodoCompletion={toggleTodoCompletion}
editID={editID}
editTodoHandler={editTodoHandler}
removeTodo={removeTodo}
state={state}
mode={mode}
isEditError={isEditError}
/>
</>
)}
</div>
</TaskContexts.Provider>
);
};
export default TaskList;
All functions on this component do not work. But these are functions. I don't understand why React doesn't think so.
You need to do 3 things to pass the context values successfully:
Place the Context Provider at least one level above the Consuming Component.
Create Your Context, Declare all variables and methods within the Context, and Export the Context's Provider after passing the value Prop.
Consume the Context Values by importing the useContext() hook in TaskList.jsx/TaskList.js and calling it on the Provider object.
Place the Context Provider at least one level above the Consuming Component
The reason JavaScript thinks editTodo is not a function or is undefined is that you are trying to consume it in React within the <TaskList/> component before it (<TaskList/>) is even made aware of the context. By the time <TaskList/> has been rendered by React, it is too late to pass any context values. So we need to place the context, somewhere higher up the component tree where React will be made aware of the context and its values ahead of time before rendering (and passing the context values to) child components down the tree.
To fix this, place the context provider wrapper at least one level above the component that is consuming the values of the context provider. If more than one component needs values from the provider, the best place to place the provider wrapper would be in your App.jsx/App.js or your index.jsx/index.js file.
Inside App.jsx/App.js:
import { TaskProvider } from 'path/to/context';
function App() {
<TaskProvider>
{/* All your code/rendered elements/rendered route elements go here */}
</TaskProvider>
}
export default App;
or Inside index.jsx/index.js:
import React from "react";
import ReactDOM from "react-dom";
import { ToastProvider } from "path/to/context";
import "./index.css";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<ToastProvider>
<App />
</ToastProvider>
</React.StrictMode>,
document.getElementById("root")
);
I'll show you a better way to pass those context values.
Create Your Context, Declare all variables and methods within the Context, and Export the Context's Provider after passing the value Prop:
Inside TaskContexts.jsx/TaskContexts.js:
import {useContext, createContext } from "react";
// ...All your necessary imports
// Create context first
const TaskContexts = createContext();
export const TaskProvider = ({ children }) => {
const [editID, setEditID] = useState(null);
const [editText, setEditText] = useState(null);
const [isEditError, setIsEditError] = useState(false);
const [mode, setMode] = useState('All');
const removeTodo = id => {
dispatch({ type: ACTION_TYPES.REMOVE, id });
};
const toggleTodoCompletion = id => {
dispatch({ type: ACTION_TYPES.TOGGLE, id });
};
const editTodo = id => {
const text = editText.trim();
try {
TODO_TASK_CHEMA.validateSync({ text });
} catch (e) {
setIsEditError(true);
throw new Error(e);
}
setIsEditError(false);
setEditID(null);
dispatch({ type: ACTION_TYPES.EDIT, id, text });
setEditText(null);
};
// ...and the rest of the methods
// Prepare your contextValues object here
const contextValues = {
editID,
setEditID,
// ...and the rest
};
// Notice that we have called the provider here
// so that we don't have to do it within the `App.jsx` or `index.jsx`.
// We have also passed the default values here so we can that
// we don't have to export them and pass them in `App.jsx`.
// We used component composition to create a `hole` where the rest of
// our app, i.e, `{children}` will go in and returned the
// composed component from here, i.e, `<TaskProvider/>`.
// This is so that all the preparation of the context Provider object
// gets done in one file.
return (<TaskContexts.Provider value={contextValues}>
{children}
</TaskContexts.Provider>);
};
// Now, use the context, we will export it in a function called `useTask()`
// so that we don't have to call `useContext(TaskContexts)` every time we need values from the context.
// This function will call `useContext()` for us and return the values
// in the provider available as long as we wrap our app components
// with the provider (which we have already done).
export function useTask() {
return useContext(TaskContexts);
}
Consume the Context Values by importing the useContext() hook in TaskList.jsx/TaskList.js and calling it on the Provider object.
Since we've already called useContext on the provider object, we just need to import useTask() from earlier in TaskList.jsx, run it and it will return the contextValues object which we can destructure.
import React, { useState } from 'react';
import ACTION_TYPES from '../ToDo/reducer/actionTypes';
import RenderedTable from './RenderedTable';
import styles from './TaskList.module.scss';
import allIcon from '../../icons/all.svg';
import completedIcon from '../../icons/completed.svg';
import notCompletedIcon from '../../icons/notCompleted.svg';
import mona from '../../icons/mona.gif';
import { TODO_TASK_CHEMA } from '../../utils/validationSchemas';
// Import `useTask` only.
import { useTask } from '../../contexts';
const TaskList = props => {
// Values from context
const {editID, setEditID,...} = useTask();
const {
reducerData: [state, dispatch],
} = props;
const [editID, setEditID] = useState(null);
const [editText, setEditText] = useState(null);
const [isEditError, setIsEditError] = useState(false);
const [mode, setMode] = useState('All');
const removeTodo = id => {
dispatch({ type: ACTION_TYPES.REMOVE, id });
};
const toggleTodoCompletion = id => {
dispatch({ type: ACTION_TYPES.TOGGLE, id });
};
const editTodo = id => {
const text = editText.trim();
try {
TODO_TASK_CHEMA.validateSync({ text });
} catch (e) {
setIsEditError(true);
throw new Error(e);
}
setIsEditError(false);
setEditID(null);
dispatch({ type: ACTION_TYPES.EDIT, id, text });
setEditText(null);
};
const editTodoHandler = ({ target: { value } }) => {
setEditText(value);
};
return (
<div className={styles.container}>
{/*...everything else */}
<RenderedTable
editTodo={editTodo}
setEditID={setEditID}
toggleTodoCompletion={toggleTodoCompletion}
editID={editID}
editTodoHandler={editTodoHandler}
removeTodo={removeTodo}
state={state}
mode={mode}
isEditError={isEditError}
/>
</>
)}
</div>
);
};
export default TaskList;
In summary, scope everything about the context object to its own component, within its own file, export it and wrap all the children components in the root component (or wrap the root component itself), and call useContext() on the provider object in the component that needs the context values.

Context API using the initial value not the one set in UseState

I just started using typescript with react and tried to deal with ContextAPI with the typescript.
So Far I've set a context and tried to use a provider inside my app.tsx.
My context file is looking like this:
import { createContext, ReactNode, useState } from "react";
type props = {
children: ReactNode
}
type GlobalContextType = {
currentValue: number;
setCurrentValue: (value: number) => void;
}
const INITIAL_VALUE = {
currentValue: 1,
setCurrentValue: () => {},
}
export const GlobalContext = createContext<GlobalContextType>(INITIAL_VALUE);
export const GlobalProvider = ({ children }: props) => {
const [currentValue, setCurrentValue] = useState(3);
return(
<GlobalContext.Provider value={{ currentValue, setCurrentValue }}>
{ children }
</GlobalContext.Provider>
)
}
while my app.tsx file is looking like this:
import './App.css';
import { useContext } from 'react';
import { GlobalContext, GlobalProvider } from './ContextAPI/GlobalContext';
function App() {
const { currentValue, setCurrentValue } = useContext(GlobalContext);
return (
<GlobalProvider>
<h1>{ currentValue }</h1>
<button onClick={() => setCurrentValue(currentValue + 1)}>Increment</button>
</GlobalProvider>
);
}
export default App;
There are few things that didn't work as I expected.
First: when I go to my localhost page it displays the inital value, which is 1, not the
one that I set using useState(3).
Second: When I click the button, it doesn't update the value.
I imagine that I'm always using the initial state value and not the one that I'm trying to set inside the provider.
Try to set the function increment in the context API then call it in the app right just after onClick event

Push state of reactive value to dependencies

I have an object: dynamicJSON that is changing. I would like to pass this object down to multiple dependencies: componentA, componentB. I also want the parts of the dependencies using the object to render when the object is changed.
I tried the useContext Hook, but received a dependency cycle error. What is the proper way to pass reactive values down to dependencies in react?
App.js
import { componetA } from "compA"
import { componetB } from "compB"
import { fetchLatestValue} from "api/fetchLatestValue"
import { useEffect } from "react"
export default function App() {
const dynamicJSON = ???;
useEffect(() => {
let timeoutId;
async function getlatestValue() {
try {
const data = await fetchLatestValue();
// update dynamicJSON here.
} catch (error) {
}
timeoutId = setTimeout(getlatestValue, 1000 * 1);
}
getlatestValue();
return () => {
clearTimeout(timeoutId);
};
}, []);
return (
<componetA />
<componetB />
);
}
compA
export default function componentA() {
const dynamicJSON = ???;
return(
<div>
{dynamicJSON.value}
</div>
)
};
Have you tried useEffect() with a dependency array? If anything in the dependency array is changed, the hook function will be triggered.
Reference: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
Sorry I mis-read your question, you should pass dynamicJSON into both components as a prop. Make dynamicJSON a state is also a good idea.
Rule of thumb: if a prop or state of a component is changed, then this component is rerendered.
import { ComponentA } from "compA";
import { ComponentB } from "compB";
import { useEffect, useState } from "react";
export default function App() {
const [dynamicJSON, setDynamicJSON] = useState({});
//...omit
return (
<ComponentA dynamicJSON={dynamicJSON}/>
<ComponentB dynamicJSON={dynamicJSON}/>
);
}
CompA.js
export default function ComponentA(props) {
const { dynamicJSON } = props;
return(
<div>
{dynamicJSON.value}
</div>
)
};

List Rendering in React JS

import React,{useState} from 'react'
import App from './App'
const AppList = ()=>{
const [arr,addElement] = useState([1])
const handleClick = ()=>{
arr.push(1)
addElement(arr)
}
return(
<>
{
arr.map((elements)=>{
return <h1>{elements}</h1>
})
}
<button onClick={handleClick}>add</button>
</>
)
}
export default AppList
I tried to add elements and render. the items are getting added in array but its not getting rendered in browser.
React is checking state updates by reference, since you provide the same array reference the component is not going to re-render.
Instead, make an immutable update
const AppList = () => {
const [arr, addElement] = useState([1])
const handleClick = () => {
addElement(prevState => [...prevState, 1])
}
return (
<>
{arr.map(element => (
<h1>{element}</h1>
))}
<button onClick={handleClick}>add</button>
</>
)
}
You also need to provide a unique key when rendering arrays, but don't use index as key since it can cause problems when adding and removing items from the array. You don't have anything unique here so you will need to think of a way of solving it. You can find more info about it here
to rerender somthing in React you need to change the state,
the best way to do it is to pass NEW array to the state, Thats why
you have the setState method.
So, you just need:
import React,{useState} from 'react'
import App from './App'
const AppList = ()=>{
const [arr,setArray] = useState([1])
const handleClick = ()=>{
let newArr = [...arr,1]
setArray(newArr)//setArray will be a better name.
}
return(
<>
{
arr.map((elements)=>{
return <h1>{elements}</h1>
})
}
<button onClick={handleClick}>add</button>
</>
)
}
export default AppList

Passing String value through Props returned from useSelector Hook

I am working on ReactJS modal dialog and bind the values from redux slice through the useSelector hook. Currently I have two functions which are already dispatching using useDispatch hook and setting the props with 2 functions(onCancelHandler, submitHandler). Here I need to keep one more field which is string value(userName) and tried to keep that and usig the string value approvedUser in DeleteUserModalContent through the props. Initially I am able to get the value from props in DeleteUserModalContent
component but when submitHandler is executed the following error is occured.
Can't read property 'userName' which is undefined
Error at this line:
const approvedUser: string = selectedUser.userName;
Can any one tell me what is wrong here?
Thanks in Advance
Code Snippet:
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Modal } from '#material-ui/core';
import { AppState } from 'store/rootReducer';
import { hideModal } from 'store/common/modalSlice';
import { submitAction } from 'store/user-actions';
import { DeleteUserModalContent } from './DeleteUserModalContent';
export const DeleteUserModal: React.FC<{}> = () => {
const dispatch = useDispatch();
const selectedUser = useSelector((state: AppState) => {
const selectedUserId =
state.selectUserSlice.selectedUsers[0];
return state.userState[selectedUserId];
});
const onCancelHandler = () => {
dispatch(hideModal());
};
const submitHandler = () => {
dispatch(
submitAction(selectedUser.userName)
);
};
const approvedUser: string = selectedUser.userName;
console.log(selectedUser.userName);
const props = {
onResetHandler,
submitHandler,
approvedUser
};
return (
<Modal>
<>
<DeleteUserModalContent {...props} />
</>
</Modal>
);
};
When we use the returned value from the useSelector hook and use the same in other component DeleteUserModalContent by setting into props. Here we are able to use the approvedUser value initially but when submitHandler function is dispatched selectedUser.userName value becomes undefined, So we can put the condition check below:
const approvedUser: string = selectedUser?.userName
to avoid the above mentioned error.

Resources