I got data.js that contains name, listOfReview that have sub nested state(name, occupation and etc). i'm using useReducer to add another review on the listofreview. but the problem is after posting a review it will create a new object outside the listofreview. The output
data.js
export const data = [
{
id: 1607089645363,
name: 'john',
noOfReview: 1,
listOfReview: [
{
reviewId: 1607089645361,
name: 'john doe',
occupation: 'hero',
rating: 5,
review: 'lorem ipsum',
}
]
},
];
index.js
import { data } from '../../src/data';
import { reducer } from './reducer';
const defaultState = {
review: data
}
const ModalHandler = props => {
const [rating, setRating] = useState(null);
const [name, setName] = useState('');
const [occupation, setOccupation] = useState('');
const [reviews, setReviews] = useState('');
const [state, dispatch] = useReducer(reducer, defaultState);
const handelSubmit = (e) => {
e.preventDefault();
if (name && occupation && reviews) {
const newReview = { Reviewid: new Date().getTime().toString(), rating, name, occupation, reviews };
dispatch({ type: 'ADD_REVIEW_ITEM', payload: newReview });
}
}
}
reducer.js
export const reducer = (state, action) => {
switch (action.type) {
case "ADD_REVIEW_ITEM":
return state.map((data) => {
if (data) {
const newReview = [...data.listOfReview, action.payload];
return {
...data,
listOfReview: newReview
};
}
return data;
});
default:
return state;
}
};
OUTPUT
According to your data structure, you have an array of listOfReview in an array of data:-
These 2 fixes may help you:-
FIXES 1: You need to pass the selected id of data you want to add new review to
FIXES 2: You need to change the way you code your reducer so that it will update the state correctly and not mutating the current state
Fixes 1
index.js. Adjust your handleSubmit to receive arg of selected id of data you want to add *new review obj. Thus, send both 'selectedDataId' and newReview via dispatch payload
import { data } from '../../src/data';
import { reducer } from './reducer';
const ModalHandler = props => {
const [rating, setRating] = useState(null);
const [name, setName] = useState('');
const [occupation, setOccupation] = useState('');
const [reviews, setReviews] = useState('');
const [state, dispatch] = useReducer(reducer, defaultState);
const handelSubmit = (e, selectedDataId) => {
e.preventDefault();
if (name && occupation && reviews) {
const newReview = { Reviewid: new Date().getTime().toString(), rating, name, occupation, reviews };
dispatch({
type: 'ADD_REVIEW_ITEM',
payload: {
selectedDataId: selectedDataId,
newReview: newReview
}
});
}
}
}
Fixes 2
in reducer. Adjust so that it will not mutate your current state
const reducer = (state, action) => {
switch (action.type) {
case "ADD_REVIEW_ITEM":
return {
...state,
review: state.review.map((data) => {
if (data.id === action.payload.selectedDataId) {
return {
...data,
listOfReview: [...data.listOfReview, action.payload.newReview]
};
}
return data;
})
};
default:
return state;
}
};
This a working sandbox of your case.
Related
I hope someone can help me with that. I'm experience the following using the React useReducer:
I need to search for items in a list.
I'm setting up a global state with a context:
Context
const defaultContext = [itemsInitialState, (action: ItemsActionTypes) => {}];
const ItemContext = createContext(defaultContext);
const ItemProvider = ({ children }: ItemProviderProps) => {
const [state, dispatch] = useReducer(itemsReducer, itemsInitialState);
const store = useMemo(() => [state, dispatch], [state]);
return <ItemContext.Provider value={store}>{children}</ItemContext.Provider >;
};
export { ItemContext, ItemProvider };
and I created a reducer in a separate file:
Reducer
export const itemsInitialState: ItemsState = {
items: [],
};
export const itemsReducer = (state: ItemsState, action: ItemsActionTypes) => {
const { type, payload } = action;
switch (type) {
case GET_ITEMS:
return {
...state,
items: payload.items,
};
default:
throw new Error(`Unsupported action type: ${type}`);
}
};
I created also a custom hook where I call the useContext() and a local state to get the params from the form:
custom hook
export const useItems = () => {
const context = useContext(ItemContext);
if (!context) {
throw new Error(`useItems must be used within a ItemsProvider`);
}
const [state, dispatch] = context;
const [email, setEmail] = useState<string>('');
const [title, setTitle] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [price, setPrice] = useState<string>('');
const [itemsList, setItemsList] = useState<ItemType[]>([]);
const onChangeEmail = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void =>
setEmail(e.currentTarget.value);
const onChangeTitle = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void =>
setTitle(e.currentTarget.value);
const onChangePrice = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void =>
setPrice(e.currentTarget.value);
const onChangeDescription = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void =>
setDescription(e.currentTarget.value);
const handleSearch = useCallback(
async (event: React.SyntheticEvent) => {
event.preventDefault();
const searchParams = { email, title, price, description };
const { items } = await fetchItemsBatch({ searchParams });
if (items) {
setItemsList(items);
if (typeof dispatch === 'function') {
console.log('use effect');
dispatch({ type: GET_ITEMS, payload: { items } });
}
}
},
[email, title, price, description]
);
// useEffect(() => {
// // add a 'type guard' to prevent TS union type error
// if (typeof dispatch === 'function') {
// console.log('use effect');
// dispatch({ type: GET_ITEMS, payload: { items: itemsList } });
// }
// }, [itemsList]);
return {
state,
dispatch,
handleSearch,
onChangeEmail,
onChangeTitle,
onChangePrice,
onChangeDescription,
};
};
this is the index:
function ItemsManagerPageHome() {
const { handleSearch, onChangeEmail, onChangePrice, onChangeTitle, onChangeDescription } = useItems();
return (
<ItemProvider>
<Box>
<SearchComponent
handleSearch={handleSearch}
onChangeEmail={onChangeEmail}
onChangePrice={onChangePrice}
onChangeTitle={onChangeTitle}
onChangeDescription={onChangeDescription}
/>
<ListContainer />
</Box>
</ItemProvider>
);
}
The ListContainer should then do this to get values from the global state:
const { state } = useItems();
The issue is that when I try to dispatch the action after the list items are fetched the reducer is not called, and I cannot figure out why.
I try to put the dispatch in a useEffect() trying to trigger it only when a listItems state changes but I can see it called only at the beginning and not when the callback is fired.
What am I doing wrong?
Thank you for the help
You should use ItemsManagerPageHome component as a descendant component of the ItemProvider component. So that you can useContext(ItemContext) to get the context value from ItemContext.Provider.
Besides, I saw you validate that useItems must be used in ItemsProvider, but the if condition always is false because the defaultContext is an array and it's always a truth value. So, your validation doesn't work. You can use a null value as the default context.
The correct way is:
context.tsx:
import { createContext, useMemo, useReducer } from 'react';
import * as React from 'react';
type ItemProviderProps = any;
type ItemsActionTypes = any;
type ItemsState = any;
export const GET_ITEMS = 'GET_ITEMS';
export const itemsInitialState: ItemsState = {
items: [],
};
export const itemsReducer = (state: ItemsState, action: ItemsActionTypes) => {
const { type, payload } = action;
switch (type) {
case GET_ITEMS:
return {
...state,
items: payload.items,
};
default:
throw new Error(`Unsupported action type: ${type}`);
}
};
const ItemContext = createContext(null);
const ItemProvider = ({ children }: ItemProviderProps) => {
const [state, dispatch] = useReducer(itemsReducer, itemsInitialState);
const store = useMemo(() => [state, dispatch], [state]);
return <ItemContext.Provider value={store}>{children}</ItemContext.Provider>;
};
export { ItemContext, ItemProvider };
hooks.ts:
import { useCallback, useContext, useState } from 'react';
import { GET_ITEMS, ItemContext } from './context';
type ItemType = any;
const fetchItemsBatch = (): Promise<{ items: ItemType[] }> =>
new Promise((resolve) =>
setTimeout(() => resolve({ items: [1, 2, 3] }), 1_000)
);
export const useItems = () => {
const context = useContext(ItemContext);
if (!context) {
throw new Error(`useItems must be used within a ItemsProvider`);
}
const [state, dispatch] = context;
const handleSearch = useCallback(async (event: React.SyntheticEvent) => {
event.preventDefault();
const { items } = await fetchItemsBatch();
if (items) {
if (typeof dispatch === 'function') {
dispatch({ type: GET_ITEMS, payload: { items } });
}
}
}, []);
return {
state,
dispatch,
handleSearch,
};
};
ItemsManagerPageHome.tsx:
import React = require('react');
import { useItems } from './hooks';
export function ItemsManagerPageHome() {
const { handleSearch, state } = useItems();
console.log('state: ', state);
return <input onClick={handleSearch} type="button" value="search" />;
}
App.tsx:
import * as React from 'react';
import { ItemProvider } from './context';
import { ItemsManagerPageHome } from './ItemsManagerPageHome';
import './style.css';
export default function App() {
return (
<div>
<ItemProvider>
<ItemsManagerPageHome />
</ItemProvider>
</div>
);
}
Demo: stackblitz
Click the "search" button and see the logs in the console.
Using redux toolkit to update the state with data from a CSV file. The state updates properly but the component only renders after the file is uploaded again.
Here is the action slice:
import { createSlice } from "#reduxjs/toolkit";
export let dataUploadSlice = createSlice({
name: "dataUpload",
initialState: {
value: [],
},
reducers: {
uploadFile: (state, action) => {
return { ...state, value: action.payload };
},
removeFile: (state) => {
state.value = [];
},
},
});
export const { uploadFile, removeFile } = dataUploadSlice.actions;
export default dataUploadSlice.reducer;
and here is the data upload component using Papaparse;
// Redux
import { useSelector, useDispatch } from "react-redux";
import { uploadFile, removeFile } from "../features/dataupload/dataUploadSlice";
const UploadDataGrid = () => {
const gridStyle = { minHeight: 440 };
const dataFile = useSelector((state) => state.dataUpload.value);
const dispatch = useDispatch();
const [colHeader, setColHeader] = useState([]);
const [dataSource, setDataSource] = useState([]);
const [fileName, setFileName] = useState("");
const handleFileUpload = (e) => {
const files = e.target.files;
Papa.parse(files[0], {
complete: function (results) {
setFileName(e.target.files[0].name);
//console.log(e.target.files[0].name);
// Dispatch the data to the table in store
dispatch(uploadFile(results.data));
let columnHeaders = dataFile[0].map((item) => {
return { name: item, header: item, minWidth: 50, defaultFlex: 1 };
});
setColHeader(columnHeaders);
let dataSources = dataFile.slice(1).map((item, index) => {
var dict = {};
for (var i = 0; i < dataFile[0].length; i++) {
dict[dataFile[0][i]] = item[i];
}
dict["id"] = index;
return dict;
});
setDataSource(dataSources);
},
});
};
dataFile in the above code only updates after I upload the file again but the state is updated correctly every time. Any idea what I might be doing wrong?
Tried other ways of assigning objects but it did not work.
In the same function/scope, you cannot get the immediate value from store that is updated with the dispatch.
Only perform dispatch in the handleFileUpload function:
const handleFileUpload = (e) => {
const { files } = e.target;
Papa.parse(files[0], {
complete(results) {
setFileName(e.target.files[0].name);
// Dispatch the data to the table in store
dispatch(uploadFile(results.data));
},
});
};
and setDataSource when dataFile is updated (dispatch updates this state). For this use useEffect:
useEffect(() => {
if (!dataFile) return;
const columnHeaders = dataFile[0].map((item) => ({
name: item,
header: item,
minWidth: 50,
defaultFlex: 1,
}));
setColHeader(columnHeaders);
const dataSources = dataFile.slice(1).map((item, index) => {
const dict = {};
for (let i = 0; i < dataFile[0].length; i++) {
dict[dataFile[0][i]] = item[i];
}
dict.id = index;
return dict;
});
setDataSource(dataSources);
}, [dataFile]);
Think dispatch like it is setState from the const [state , setState] = useState() and the value from useSelector like the state value. First you need to set the state and watch this update within useEffect like hook that has dependency array.
So I am building an e-commerce website checkout page with commerce.js. I have a context that allows me to use the cart globally. But on the checkout page when I generate the token inside useEffect , the cart variables have not been set until then.
My context is as below
import { createContext, useEffect, useContext, useReducer } from 'react';
import { commerce } from '../../lib/commerce';
//Provides a context for Cart to be used in every page
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = 'SET_CART';
const initialState = {
id: '',
total_items: 0,
total_unique_items: 0,
subtotal: [],
line_items: [{}],
};
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, initialState);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
useEffect(() => {
getCart();
}, []);
const getCart = async () => {
try {
const cart = await commerce.cart.retrieve();
setCart(cart);
} catch (error) {
console.log('error');
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={state}>
{children}
</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
Now on my checkout page
const CheckoutPage = () => {
const [open, setOpen] = useState(false);
const [selectedDeliveryMethod, setSelectedDeliveryMethod] = useState(
deliveryMethods[0]
);
const [checkoutToken, setCheckoutToken] = useState(null);
const { line_items, id } = useCartState();
useEffect(() => {
const generateToken = async () => {
try {
const token = await commerce.checkout.generateToken(id, {
type: 'cart',
});
setCheckoutToken(token);
} catch (error) {}
};
console.log(checkoutToken);
console.log(id);
generateToken();
}, []);
return <div> {id} </div>; //keeping it simple just to explain the issue
};
In the above code id is being rendered on the page, but the token is not generated since on page load the id is still blank. console.log(id) gives me blank but {id} gives the actual value of id
Because CheckoutPage is a child of CartProvider, it will be mounted before CartProvider and the useEffect will be called in CheckoutPage first, so the getCart method in CartProvider hasn't been yet called when you try to read the id inside the useEffect of CheckoutPage.
I'd suggest to try to call generateToken each time id changes and check if it's initialised first.
useEffect(() => {
if (!id) return;
const generateToken = async () => {
try{
const token = await commerce.checkout.generateToken(id, {type: 'cart'})
setCheckoutToken(token)
} catch(error){
}
}
console.log(checkoutToken)
console.log(id)
generateToken()
}, [id]);
I'm trying to filter items by their category using useReducer
context.jsx
const initialState = {
categoryName: "all item",
};
const [state, dispatch] = useReducer(reducer, initialState);
const fetchUrl = async () => {
const resp = await fetch(url);
const respData = await resp.json();
const item = respData.item;
const category = respData.category;
const promo = respData.promo;
dispatch({ type: "CATEGORY_ITEM", payload: category });
};
I want to display the category name that matched the data.
reducer.jsx
if (action.type === "FILTER_NAME") {
if (action.payload === "all menu") {
return { ...state, categoryName: "all menu" };
//return { ...state, categoryName: state.categoryName};
} else {
return { ...state, categoryName: action.payload };
}
}
I cant set the categoryName back to the state value because it's been changed when I do else.
Is there a way for me to set a default value in reducer? Because if I use useState the setState won't overwrite the state default value.
Thanks before
I'm new to react-redux and hooks. I'm building this simple CRUD employee management app, and I'm trying update an employee but I can't seem to make it work. here is my code..
const EditEmployee = ({history, match}) => {
const empById = match.params.id
const dispatch = useDispatch()
const emp = useSelector((state) => state.employee)
const {loading, employee} = emp
const [full_name, setFull_name] = useState('');
const [email, setEmail] = useState('');
const [phone_number, setPhone_number] = useState('');
const [address, setAddress] = useState('');
useEffect(() => {
dispatch(getSingleEmployee(empById))
}, [dispatch, empById])
useEffect(() => {
if (employee != null) {
setFull_name(employee.full_name)
setEmail(employee.email)
setPhone_number(employee.phone_number)
setAddress(employee.address)
}
}, [employee])
const onSubmit = (e) => {
e.preventDefault()
const updateEmp = {
empById,
full_name,
email,
phone_number,
address
}
dispatch(updateEmployees(updateEmp))
history.push('/home')
}
Then it's giving me this Error
SequelizeDatabaseError: invalid input syntax for type bigint: "[object Object]"
Can someone please asssit.
And here is the backend..
Model:
import Sequelize from "sequelize"
import db from "../config/database.js";
const Employee = db.define('employee', {
full_name: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
phone_number: {
type: Sequelize.STRING
},
address: {
type: Sequelize.STRING
},
})
export default Employee
Controller:
const updateEmployee = (req, res) => {
const id = req.params.id;
const updates = req.body
Employee.findOne({
where: { id: id }
})
.then(employee => {
if (employee) {
return employee.update(updates)
} else {
res.status(404).json({message: 'Employee not found'})
throw new Error('Employee not found')
}
})
.then(updatedEmployee => {
res.status(201).json(updatedEmployee);
});
}
I think I did everything correctly, in postman everything seems to work