React and state - reactjs

I would like your take on a specific implementation. I have a react app (no redux), the app has a shopping cart. The shopping cart is defined in the state in the App component and it is passed and used further down the tree in several components. E.g. I have a component called ShoppingCart, it displays the shopping cart, plus it has actions to add/remove/clear the cart.
My problem is updating the shopping cart state after performing an action on the shopping cart. E.g. when I call a function to clear the shopping cart, the state should be updated in the App component thus updating my component which is further down the tree. How would one implement these action functions (without redux)?
Code:
const App = () => {
const [cart, setCart] = useState({ lines: [], total: 0 });
return <ShoppingCart cart={cart} />;
}
const ShoppingCart = ({ cart }) => {
const onAddOne = l => {
// not sure how to update cart and update state
}
const onRemoveOne = l => {
// not sure how to update cart and update state
}
return (
<table>
{
cart.lines.map(l => <tr><td>{l.name}</td><td><button onClick={() => onAddOne(l)}>+</button><button onClick={() => onRemoveOne(l)}>-</button></td></tr>)
}
</table>
);
}
Thanks in advance for any tip.

Here you can use the useContext hook.
The idea is similar to redux.
So, what you can do is, first create a StateProvider, like in the example
import React, { createContext, useReducer, useContext } from "react";
export const StateContext = createContext();
export const StateProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
);
export const useStateValue = () => useContext(StateContext);
Similarly, create a Reducer for that, you can add more reducers, the example shown is to ADD ITEMS IN BASKET and REMOVE ITEMs FROM BASKET
export const initialState = {
basket: [],
user: null,
};
export const getBasketTotal = (basket) =>
basket?.reduce((amount, item) => item.price + amount, 0);
function reducer(state, action) {
switch (action.type) {
case "ADD_TO_BASKET":
return { ...state, basket: [...state.basket, action.item] };
case "REMOVE_ITEM":
let newBasket = [...state.basket];
const index = state.basket.findIndex(
(basketItem) => basketItem.id === action.id
);
if (index >= 0) {
newBasket.splice(index, 1);
} else {
console.warn("Cant do this");
}
return { ...state, basket: newBasket };
default:
return state;
}
}
export default reducer;
Go to your index.js file and wrap your file like this
<StateProvider initialState={initialState} reducer={reducer}>
<App />
</StateProvider>
And voila, while adding items to the basket use following code
const addtobasket = () => {
dispatch({
type: "ADD_TO_BASKET",
item: {
id: id,
title: title,
price: price,
rating: rating,
color: color,
},
});
};

I found a solution, however, I am not sure it is the correct way to do things:
const App = () => {
const onUpdateCart = (cart) => {
setCart({ ...cart });
}
const [cart, setCart] = useState({ lines: [], total: 0, onUpdateCart });
return <ShoppingCart cart={cart} />;
}
const ShoppingCart = ({ cart }) => {
const onRemoveLine = l => {
cart.lines = cart.lines.filter(l2 => l2 !== l);
cart.onUpdateCart(cart);
}
const onAddOne = l => {
l.amount++;
cart.onUpdateCart(cart);
}
const onRemoveOne = l => {
l.amount--;
cart.onUpdateCart(cart);
}
return (
<table>
{
cart.lines.map(l => (
<tr>
<td>{l.name}</td>
<td>
<button onClick={() => onAddOne(l)}>+</button>
<button onClick={() => onRemoveOne(l)}>-</button>
<button onClick={() => onRemoveLine(l)}>x</button>
</td>
</tr>)
)
}
</table>
);
};

The straight forward way to implement this is to pass down props to the child component that when called update the state.
Notice how all state business logic is in a central place .e.g in App component. This allows ShoppingCart to be a much simpler.
const App = () => {
const [cart, setCart] = useState({ lines: [], total: 0 });
const updateLineAmount = (lineIdx, amount) => {
// update the amount on a specific line index
setCart((state) => ({
...state,
lines: state.lines.map((line, idx) => {
if (idx !== lineIdx) {
return line;
}
return {
...line,
amount: line.amount + amount,
};
}),
}));
};
const onAddOne = (lineIdx) => {
updateLineAmount(lineIdx, 1);
};
const onRemoveOne = (lineIdx) => {
updateLineAmount(lineIdx, -1);
};
return (
<ShoppingCart cart={cart} onAddOne={onAddOne} onRemoveOne={onRemoveOne} />
);
};
const ShoppingCart = ({ cart, onAddOne, onRemoveOne }) => {
return (
<table>
{cart.lines.map((line, idx) => (
<tr key={idx}>
<td>{line.name}</td>
<td>
<button onClick={() => onAddOne(idx)}>+</button>
<button onClick={() => onRemoveOne(idx)}>-</button>
</td>
</tr>
))}
</table>
);
};

Related

Why the list doesn't re-render after updating the store?

I started learning mobx and got stuck. Why when I change listItems, List doesn't re-render?
I have store:
export const listStore = () => {
return makeObservable(
{
listItems: [],
addItem(text) {
this.listItems.push(text);
}
},
{
listItems: observable,
addItem: action.bound
}
);
};
Component that adds text from input to store:
const store = listStore();
export const ListForm = observer(() => {
const [value, setValue] = useState();
return (
<>
<input type="text" onChange={e => setValue(e.target.value)} />
<button onClick={() => store.addItem(value)}>Add note</button>
</>
);
});
And I have a list component:
const store = listStore();
export const List = () => {
return (
<React.Fragment>
<ul>
<Observer>
{() => store.listItems.map(item => {
return <li key={item}>{item}</li>;
}
</Observer>
</ul>
<ListForm />
</React.Fragment>
);
};
I don't understand what's wrong. Looks like the list doesn't watch the store changing
codesandbox: https://codesandbox.io/s/ancient-firefly-lkh3e?file=/src/ListForm.jsx
You create 2 different instances of the store, they don't share data between. Just create one singleton instance, like that:
import { makeObservable, observable, action } from 'mobx';
const createListStore = () => {
return makeObservable(
{
listItems: [],
addItem(text) {
this.listItems.push(text);
}
},
{
listItems: observable,
addItem: action.bound
}
);
};
export const store = createListStore();
Working example

Checking one checkbox is checking all other checkboxes

I have a checkbox component whose state is handled in Redux Toolkit. When checking one checkbox it's checking all other checkboxes in all rows, but I only want to check the checkbox I click on.
Here's my code below:
Checkbox Component:
export const Checkbox = (props) => {
const dispatch = useDispatch()
const handleCheckBox = () => {
dispatch(checkboxState)
}
const isChecked = useSelector((state) => (
state.isChecked
))
return (
<input type='checkbox' checked={isChecked} onChange={handleCheckBox}/>
)
}
Slice:
const rowState = {
data: [],
isChecked: false,
loading: false
}
export const rowSlice = createSlice({
name: 'rows',
initialState: rowState,
reducers: {
CHECK_ROWS(state) {
state.isChecked = !state.isChecked
},
})
export const checkboxState = rowSlice.actions.CHECK_ROWS()
Then I'm calling the checkbox component in my page:
const handleRows = (rowData) => {
return (
<tr>
<td>
<Checkbox />
</td>
//rest of the code
</tr>
return(
<Table>
{
dataSource.map((data) => (
handleRows(data)
))
}
</Table>
)
This is happening because you are keeping one variable isChecked for the entire component. To make it unique to each data, keep this as an array:
const rowState = {
data: [],
checkedData: [],
loading: false
}
Then, you should update the checkedData array accordingly. Check state will receive an id or an index and remove from checkedData if it is present in checkecData or add to checkedData if it is not present.
An example:
checkedData.includes(index) ? checkedData.filter((d,i) => i !== index) : [...checkedData, index]
Each checkbox should need an index, and the state should keep track of which checkbox that is checked or not.
import { useSelector, Provider, useDispatch } from "react-redux";
import { createAction, createReducer, configureStore } from "#reduxjs/toolkit";
const initialState = { checkboxes: [false, false, false] };
const index = createAction("toggle/index");
const toggleReducer = createReducer(initialState, (builder) => {
builder.addCase(index, (state, action) => {
state.checkboxes[action.payload] = !state.checkboxes[action.payload];
});
});
const store = configureStore({ reducer: toggleReducer });
const toggleIndex = (index) => {
return {
type: "toggle/index",
payload: index,
};
};
export const Checkbox = ({ index }) => {
const dispatch = useDispatch();
const handleCheckBox = () => {
dispatch(toggleIndex(index));
};
const isChecked = useSelector(({ checkboxes }) => checkboxes[index]);
return (
<input type="checkbox" checked={isChecked} onChange={handleCheckBox} />
);
};
const App = () => {
return (
<Provider store={store}>
<div>
<Checkbox index={0} />
<Checkbox index={1} />
<Checkbox index={2} />
</div>
</Provider>
);
};
export default App;

How can I create another state inside Redux that contains two reducers to update the state

I'm creating a movie app using React/Redux that allows the user to choose a movie by clicking on a button (buy a ticket ) that takes the user to another page to choose the quantity of the ticket and add to the cart his purchase.
The idea is when the user clicks on the button add to card, I want two things to happen one is that the quantity should be updated in the badge cart in the navBar and also add this the movie to the bag if the user wants to checkout.
How can I create a state called cart in reducers to update the quantity and to add the movie into that cart when I click on add to a cart?
What do you think?
<-- Action types -->
export const ActionsTypes = {
SET_MOVIES : "SET_MOVIES",
GET_MOVIE : "GET_MOVIE",
REMOVE_MOVIE : "REMOVE_MOVIE",
QUANTITY: "QUANTITY",
}
<-- Quantity action-->
export const MovieQuantity = () => {
return {
type : ActionsTypes.QUANTITY
}
}
<-- Reducers -->
const initialState = {
movies: [],
};
//Movies Reducers
export const setMoviesReducers = (state = initialState, action) => {
switch (action.type) {
case ActionsTypes.SET_MOVIES:
return {...state, movies: action.payload }
default:
return state;
}
}
// single Movies Reducers
export const GetMovieDetailsReducers = (state={}, action) => {
switch(action.type) {
case ActionsTypes.GET_MOVIE :
return {...state, ...action.payload}
case ActionsTypes.REMOVE_MOVIE :
return {};
default :
return state
}
}
export const movieQuantityReducers = (state = 0 , action) => {
switch(action.type) {
case ActionsTypes.QUANTITY:
return state + 1;
default :
return state;
}
}
<-- Movie Details add to cart component -->
const MovieDetails = () => {
const [quantity, setQuantity] = useState(1)
const singleMovie = useSelector((state)=> state.movie);
const quantityBag = useSelector((state)=> state.quantity);
const {title, poster_path, overview} = singleMovie;
const dispatch = useDispatch();
let {movieId} = useParams();
// Handle Click Quantity
const handleQuantity = (type) => {
if(type === "dec") {
quantity > 1 && setQuantity(quantity - 1)
} else {
setQuantity(quantity + 1)
}
}
// add to cart Handler
const CartHandler = () => {
dispatch(MovieQuantity(quantityBag)) // the quantity is just incrementing
}
// Get a single Product & Remove product
useEffect(()=> {
try {
const getSingleMovie = async () => {
const request = await axios.get(`https://api.themoviedb.org/3/movie/${movieId}?api_key=&&&&&`);
const response = await request.data;
dispatch(getMovie(response))
}
getSingleMovie();
} catch(error) {
console.log(`ERROR : ${error}`)
}
//Clean up
return () => {
dispatch(removeMovie());
}
}, [movieId])
return (
<section className="movieDetails_container">
<div className="wrapper">
<div className="img-container">
<img src={`${ImgPath}` + poster_path} alt={title}/>
</div>
<div className="info-container">
<h1>{title}</h1>
<p>{overview}</p>
<div className="quantity-container">
<Remove className="quantity-icon" onClick={()=> handleQuantity("dec")}/>
<span className="amount">{quantity}</span>
<Add className="quantity-icon" onClick={()=> handleQuantity("incr")}/>
</div>
<button className="btn-add" onClick={()=> CartHandler()}>Add To Cart</button>
</div>
</div>
</section>
)
}
export default MovieDetails
<-- navBar componenet -->
const Navbar = () => {
const quantityBag = useSelector((state)=> state.quantity);
return (
<nav className="navBar-section">
<Link to="/">
<h1 className="logo">映画館</h1>
</Link>
<Badge badgeContent={quantityBag} color="primary">
<LocalMall className="icon-bag" />
</Badge>
</nav>
)
}
export default Navbar
I'd model the state differently. From what you're describing, the following would work nicely:
const state = {
// movie details reducer handles this part:
movies: {
"movie1Id": {
"name": "Title of movie1"
// more movie details here
},
"movie2Id": {
"name": "Title of movie2"
// more movie details here
},
},
// cart reducer handles this part:
cart: {
"movie1Id": 3, // quantity. user chose 3 tickets for movie1.
"movie2Id": 1, // quantity. user chose 1 ticket for movie2.
}
};
const setCartItemAction = (movieId, quantity) => ({
type: "SET_CART_ITEM",
payload: { movieId, quantity }
});
The setCartItemAction would be enough to model your use case. Calling it with a quantity of 0 for a certain movie would be the same as removing the movie from the cart.

How to Revert to the Original Background Color of previously Clicked Components When Clicking Another in React

What I am trying to achieve is, as mentioned in the title, to revert the component's background color when another entry component in the sidebar gets clicked. I use React Context API for state management. The initial state contains an array of objects named numbers which has two elements (id & number) and current. On Sidebar.js, it renders the SidebarEntry component iterating the numbers array.
I know why I am stuck at this point. It is because there is no way that I can change the state of the previously clicked component unless it gets clicked again. The following code snippet is what I reproduced my issue.
reducer.js
export const initialState = {
numbers: [
{
id: 1,
number: 101
},
{
id: 2,
number: 102
},
{
id: 3,
number: 103
},
],
current: null
}
const reducer = (state, action) => {
switch(action.type) {
case 'CHANGE_STATE':
return {
...state,
current: action.current
};
default:
return state;
}
}
export default reducer;
StateProvider.js (This wraps in index.js with initialState and reducer arguments)
import React, { createContext, useContext, useReducer } from "react";
export const StateContext = createContext();
export const StateProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
);
export const useStateValue = () => useContext(StateContext);
Sidebar.js
const Sidebar = () => {
const [{ numbers }] = useStateValue();
return (
<div className="sidebar">
{
numbers.map(number => (
<SidebarEntry key={number.id} number = {number.number} />
))
}
</div>
)
}
export default Sidebar
SidebarEntry.js
const SidebarEntry = ({ number }) => {
const [{ current }, dispatch] = useStateValue();
const [selected, setSelected] = useState(false);
const changeState = (e) => {
e.preventDefault();
dispatch({
type: "CHANGE_STATE",
current: {number}
});
setSelected(!selected);
}
return (
<div className="sidebarEntry">
<h3
className={selected && "sidebarEntry__color"}
onClick={changeState}
>
{number}
</h3>
</div>
)
}
export default SidebarEntry
Sidebar & SidebarEntry
When clicking 101, then clicking 102 (101's background color is supposed to be back to gray)
The problem is that when you dispatch a new active value in redux, you are actually changing the redux state.
But you are not changing the current state of the component selected.
I would delete this state altogether and do it like this:
const SidebarEntry = ({ number }) => {
const [{ current }, dispatch] = useStateValue();
const changeState = (e) => {
e.preventDefault();
dispatch({
type: "CHANGE_STATE",
current: {number}
});
setSelected(!selected);
}
return (
<div className="sidebarEntry">
<h3
className={current === number && "sidebarEntry__color"}
onClick={changeState}
>
{number}
</h3>
</div>
)
}
export default SidebarEntry
This should work for you

What's the best way to use react-redux with list of items?

I have a list of items (JSON objects returned from API) in a redux store. This data is normalized currently, so it's just an array. It'll roughly have 10-30 objects and each object will have about 10 properties.
Currently we have a top level container (uses react-redux connect) that reads this list from the store, maps over the array and renders a component called ListItem, which basically needs 3-4 fields from the object to render the UI.
We don't have any performance issue with this now. But I wonder if it makes sense to have a redux container component for each list item? I think this will require data to be normalized and we'd need the unique id of each object to be passed to this container which can then read the object from redux store?
This question arises from the Redux docs' Style Guide - https://redux.js.org/style-guide/style-guide#connect-more-components-to-read-data-from-the-store
Just trying to understand which is the recommended way to use react-redux in this scenario.
Thanks!
I wonder if it makes sense to have a redux container component for each list item?
Besides possibly better performance there is also better code reuse. If the logic of what an item is is defined in the list then how can you reuse the list to render other items?
Below is an example where item is a combination of data and edit so props for item will be recreated for all items if you'd create the props in List instead of Item.
List can also not be used as a general list that passes id to Item component.
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { useMemo } = React;
const { createSelector } = Reselect;
const { produce } = immer;
const initialState = {
people: {
data: {
1: { id: 1, name: 'Jon' },
2: { id: 2, name: 'Marie' },
},
edit: {},
},
places: {
data: {
1: { id: 1, name: 'Rome' },
2: { id: 2, name: 'Paris' },
},
edit: {},
},
};
//action types
const SET_EDIT = 'SET_EDIT';
const CANCEL_EDIT = 'CANCEL_EDIT';
const SAVE = 'SAVE';
const CHANGE_TEXT = 'CHANGE_TEXT';
//action creators
const setEdit = (dataType, id) => ({
type: SET_EDIT,
payload: { dataType, id },
});
const cancelEdit = (dataType, id) => ({
type: CANCEL_EDIT,
payload: { dataType, id },
});
const save = (dataType, item) => ({
type: SAVE,
payload: { dataType, item },
});
const changeText = (dataType, id, field, value) => ({
type: CHANGE_TEXT,
payload: { dataType, id, field, value },
});
const reducer = (state, { type, payload }) => {
if (type === SET_EDIT) {
const { dataType, id } = payload;
return produce(state, (draft) => {
draft[dataType].edit[id] = draft[dataType].data[id];
});
}
if (type === CANCEL_EDIT) {
const { dataType, id } = payload;
return produce(state, (draft) => {
delete draft[dataType].edit[id];
});
}
if (type === CHANGE_TEXT) {
const { dataType, id, field, value } = payload;
return produce(state, (draft) => {
draft[dataType].edit[id][field] = value;
});
}
if (type === SAVE) {
const { dataType, item } = payload;
return produce(state, (draft) => {
const newItem = { ...item };
delete newItem.edit;
draft[dataType].data[item.id] = newItem;
delete draft[dataType].edit[item.id];
});
}
return state;
};
//selectors
const createSelectData = (dataType) => (state) =>
state[dataType];
const createSelectDataList = (dataType) =>
createSelector([createSelectData(dataType)], (result) =>
Object.values(result.data)
);
const createSelectDataById = (dataType, itemId) =>
createSelector(
[createSelectData(dataType)],
(dataResult) => dataResult.data[itemId]
);
const createSelectEditById = (dataType, itemId) =>
createSelector(
[createSelectData(dataType)],
(dataResult) => (dataResult.edit || {})[itemId]
);
const createSelectItemById = (dataType, itemId) =>
createSelector(
[
createSelectDataById(dataType, itemId),
createSelectEditById(dataType, itemId),
],
(item, edit) => ({
...item,
...edit,
edit: Boolean(edit),
})
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const Item = ({ item, dataType }) => {
const dispatch = useDispatch();
return (
<li>
{item.edit ? (
<React.Fragment>
<input
type="text"
value={item.name}
onChange={(e) =>
dispatch(
changeText(
dataType,
item.id,
'name',
e.target.value
)
)
}
/>
<button
onClick={() =>
dispatch(cancelEdit(dataType, item.id))
}
>
cancel
</button>
<button
onClick={() => dispatch(save(dataType, item))}
>
save
</button>
</React.Fragment>
) : (
<React.Fragment>
{item.name}
<button
onClick={() =>
dispatch(setEdit(dataType, item.id))
}
>
edit
</button>
</React.Fragment>
)}
</li>
);
};
const createItem = (dataType) =>
React.memo(function ItemContainer({ id }) {
const selectItem = useMemo(
() => createSelectItemById(dataType, id),
[id]
);
const item = useSelector(selectItem);
return <Item item={item} dataType={dataType} />;
});
const Person = createItem('people');
const Location = createItem('places');
const List = React.memo(function List({ items, Item }) {
return (
<ul>
{items.map(({ id }) => (
<Item key={id} id={id} />
))}
</ul>
);
});
const App = () => {
const [selectPeople, selectPlaces] = useMemo(
() => [
createSelectDataList('people'),
createSelectDataList('places'),
],
[]
);
const people = useSelector(selectPeople);
const places = useSelector(selectPlaces);
return (
<div>
<List items={people} Item={Person} />
<List items={places} Item={Location} />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://unpkg.com/immer#7.0.5/dist/immer.umd.production.min.js"></script>
<div id="root"></div>
If your application has repeating logic you may want to think of splitting the component up in container and presentation (container also called connected component in redux). You can re use the container but change the presentation.

Resources