MUI Snackbar Rerenders Again When Store Gets Updated - reactjs

I am using MUI Snackbar in my root component (using STORE) and it appeared more than once because when store updated, the component gets re-rendered again
Snackbar Component:
export type BaseLayoutProps = {
children: ReactNode;
};
const Toast: FC<BaseLayoutProps> = ({ children }) => {
const [state,] = useSetupsStore();
const { toastProps } = state;
const [open, setOpen] = useState(toastProps?.toastState);
useEffect(() => {
if (toastProps) {
setOpen(toastProps.toastState);
}
}, [toastProps]);
const handleClose = (
event: React.SyntheticEvent | Event,
reason?: string
) => {
if (reason === "clickaway") {
return;
}
setOpen(false);
};
return (
<>
<Snackbar
theme={unityTheme}
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
autoHideDuration={
toastProps?.toastLength ? toastProps.toastLength : 5000
}
variant={toastProps?.variant}
open={open}
onClose={handleClose as any}
message={toastProps?.toastMessage ? toastProps?.toastMessage : "Testing"}
TransitionComponent={(props) => <Slide {...props} direction="up" />}
/>
{children}
</>
)
}
export default Toast;
SnackBar Action:
export const setToast = (toastProps: IToast): StoreActionType => {
if (toastProps?.toastState) {
return { type: ActionTypes.SET_TOAST, payload: toastProps };
}
};
Snackbar Reducer
export const ToastReducer = (
state: IToast,
action: StoreActionType
): IToast => {
switch (action.type) {
case ActionTypes.SET_TOAST:
return {
...state,
toastState: (action.payload as IToast).toastState,
variant: (action.payload as IToast).variant,
toastMessage: (action.payload as IToast).toastMessage,
// toastLength: (action.payload as IToast).toastLength,
}
default:
return state
}
};
Dispatch Code:
dispatch(
setToast({
toastMessage: ToastMessages.Record_Added_Success,
toastState: true,
})
);
I'm 100% sure that it is dispatching only once but when another api get called(store get updated) it again comes to my reducer and root component get re-rendered again due to which it appeared twice. I have also return null in default case in reducer like this:
export const ToastReducer = (
state: IToast,
action: StoreActionType
): IToast => {
switch (action.type) {
case ActionTypes.SET_TOAST:
console.log("inside case",action);
return {
...state,
toastState: (action.payload as IToast).toastState,
variant: (action.payload as IToast).variant,
toastMessage: (action.payload as IToast).toastMessage,
}
default:
return null
}
In above case Snackbar appeared only once for a milisecond & then disappeared again
How to resolve this?

Related

How can I implement not only one but multi setting toggles?

I use Shopify Polaris's setting toggle.https://polaris.shopify.com/components/actions/setting-toggle#navigation
And I want to implement not only one but multi setting toggles.But I don't want to always duplicate same handleToggle() and values(contentStatus, textStatus) like below the sandbox A,B,C...
import React, { useCallback, useState } from "react";
import { SettingToggle, TextStyle } from "#shopify/polaris";
export default function SettingToggleExample() {
const [activeA, setActiveA] = useState(false);
const [activeB, setActiveB] = useState(false);
const handleToggleA = useCallback(() => setActiveA((active) => !active), []);
const handleToggleB = useCallback(() => setActiveB((active) => !active), []);
const contentStatusA = activeA ? "Deactivate" : "Activate";
const contentStatusB = activeB ? "Deactivate" : "Activate";
const textStatusA = activeA ? "activated" : "deactivated";
const textStatusB = activeB ? "activated" : "deactivated";
const useHandleToggle = (active, setActive) => {
const handleToggle = useCallback(() => setActive((active) => !active), []);
const contentStatus = active ? "Disconnect" : "Connect";
const textStatus = active ? "connected" : "disconnected";
handleToggle();
return [contentStatus, textStatus];
};
useHandleToggle(activeA, setActiveA);
return (
<>
<SettingToggle
action={{
content: contentStatusA,
onAction: handleToggleA
}}
enabled={activeA}
>
This setting is <TextStyle variation="strong">{textStatusA}</TextStyle>.
</SettingToggle>
<SettingToggle
action={{
content: contentStatusB,
onAction: handleToggleB
}}
enabled={activeB}
>
This setting is <TextStyle variation="strong">{textStatusB}</TextStyle>.
</SettingToggle>
</>
);
}
https://codesandbox.io/s/vigorous-pine-k0dpib?file=/App.js
So I thought I can use a custom hook. But it's not working. So it would be helpful if you give me some advice.
Using simple Booleans for each toggle
If you combine your active state objects into a single array, then you can update as many settings as you would like dynamically. Here's an example of what that might look like:
import React, { useCallback, useState } from "react";
import { SettingToggle, TextStyle } from "#shopify/polaris";
export default function SettingToggleExample() {
// define stateful array of size equal to number of toggles
const [active, setActive] = useState(Array(2).fill(false));
const handleToggle = useCallback((i) => {
// toggle the boolean at index, i
setActive(prev => [...prev.slice(0,i), !prev[i], ...prev.slice(i+1)])
}, []);
return (
<>
{activeStatuses.map((isActive, index) =>
<SettingToggle
action={{
content: isActive ? "Deactivate" : "Activate",
onAction: () => handleToggle(index)
}}
enabled={isActive}
>
This setting is <TextStyle variation="strong">{isActive ? "activated" : "deactivated"}</TextStyle>.
</SettingToggle>
}
</>
);
}
Of course, you will likely want to add a label to each of these going forward, so it may be better to define a defaultState object outside the function scope and replace the Array(2).fill(false) with it. Then you can have a string label property for each toggle in addition to a boolean active property which can be added next to each toggle in the .map(...).
With labels added for each toggle
Per your follow up, here is the implementation also found in the CodeSandbox for a state with labels for each toggle (including here on the answer to protect against link decay):
import React, { useCallback, useState } from "react";
import { SettingToggle, TextStyle } from "#shopify/polaris";
const defaultState = [
{
isActive: false,
label: "A"
},
{
isActive: false,
label: "B"
},
{
isActive: false,
label: "C"
}
];
export default function SettingToggleExample() {
const [active, setActive] = useState(defaultState);
const handleToggle = useCallback((i) => {
// toggle the boolean at index, i
setActive((prev) => [
...prev.slice(0, i),
{ ...prev[i], isActive: !prev[i].isActive },
...prev.slice(i + 1)
]);
}, []);
return (
<div style={{ height: "100vh" }}>
{active?.map(({ isActive, label }, index) => (
<SettingToggle
action={{
content: isActive ? "Deactivate" : "Activate",
onAction: () => handleToggle(index)
}}
enabled={isActive}
key={index}
>
This {label} is 
<TextStyle variation="strong">
{isActive ? "activated" : "deactivated"}
</TextStyle>
.
</SettingToggle>
))}
</div>
);
}
My first attempt to refactor would use a parameter on the common handler
const handleToggle = useCallback((which) => {
which === 'A' ? setActiveA((activeA) => !activeA)
: setActiveB((activeB) => !activeB)
},[])
...
<SettingToggle
action={{
content: contentStatusA,
onAction: () => handleToggle('A')
}}
enabled={activeA}
>
It functions, but feels a bit naïve. For something more React-ish, a reducer might be the way to go.
With a reducer
This seems cleaner, and is definitely more extensible if you need more toggles.
function reducer(state, action) {
switch (action.type) {
case "toggleA":
const newValueA = !state.activeA;
return {
...state,
activeA: newValueA,
contentStatusA: newValueA ? "Deactivate" : "Activate",
textStatusA: newValueA ? "activated" : "deactivated"
};
case "toggleB":
const newValueB = !state.activeB;
return {
...state,
activeB: newValueB,
contentStatusB: newValueB ? "Deactivate" : "Activate",
textStatusB: newValueB ? "activated" : "deactivated"
};
default:
throw new Error();
}
}
const initialState = {
activeA: false,
activeB: false,
contentStatusA: "Activate",
contentStatusB: "Activate",
textStatusA: "deactivated",
textStatusB: "deactivated"
};
export default function SettingToggleExample() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<>
<SettingToggle
action={{
content: state.contentStatusA,
onAction: () => dispatch({type: 'toggleA'})
}}
enabled={state.activeA}
>
This setting is <TextStyle variation="strong">{state.textStatusA}</TextStyle>.
</SettingToggle>
<SettingToggle
action={{
content: state.contentStatusB,
onAction: () => dispatch({type: 'toggleA'})
}}
enabled={state.activeB}
>
This setting is <TextStyle variation="strong">{state.textStatusB}</TextStyle>.
</SettingToggle>
</>
);
}
With a wrapper component
A child component can eliminate the 'A' and 'B' suffixes
function reducer(state, action) {
switch (action.type) {
case "toggle":
const newValue = !state.active;
return {
...state,
active: newValue,
contentStatus: newValue ? "Deactivate" : "Activate",
textStatus: newValue ? "activated" : "deactivated"
};
default:
throw new Error();
}
}
const initialState = {
active: false,
contentStatus: "Activate",
textStatus: "deactivated",
};
const ToggleWrapper = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<SettingToggle
action={{
content: state.contentStatus,
onAction: () => dispatch({ type: "toggle" })
}}
enabled={state.active}
>
This setting is <TextStyle variation="strong">{state.textStatus}</TextStyle>.
</SettingToggle>
)
}
export default function SettingToggleExample() {
return (
<>
<ToggleWrapper />
<ToggleWrapper />
</>
);
}

Reactjs getting a state value from a child (with hookReducer) to parent component

I'm building a simple counter using
useReducer to manage the state up to here
no problem.
I'm not sure how to pass the
current state to the parent component
if I try to use the handler in the click event like
const handlerMinus = () => {
dispatch({ type: Action.MINUS });
handler(state);
};
but I get the old value.
So I manage to use useEffect
// Update
useEffect(() => {
if (!firstMounded.current) {
handler(state.value);
}
firstMounded.current = false;
}, [state]);
It seems to work but I'm not very proud of it :)
Is there a better standard way to get the state from the parent?
All code
export interface CounterState {
value: number;
}
export interface CounterProps {
color?: string;
value?: number | string;
handler: (value: number) => void;
}
enum Action {
MINUS = "ADD",
PLUS = "DELETE",
RESET = "DONE"
}
type Actions =
| { type: Action.MINUS }
| { type: Action.PLUS }
| { type: Action.RESET };
const initialCounterState: CounterState = { value: 0 };
const reducer = (state: CounterState, action: Actions): CounterState => {
switch (action.type) {
case Action.MINUS:
return { value: state.value > 0 ? state.value - 1 : state.value };
case Action.PLUS:
return { value: state.value + 1 };
case Action.RESET:
return { value: 0 };
default:
return state;
}
};
const Counter = ({ color, value, handler }: CounterProps) => {
if (value) {
initialCounterState.value = Number(value);
}
const [state, dispatch] = useReducer(reducer, initialCounterState);
const firstMounded = useRef(true);
const c = color ?? "gray.500";
useEffect(() => {
if (!firstMounded.current) {
handler(state.value);
}
firstMounded.current = false;
}, [state]);
const handlerMinus = () => {
dispatch({ type: Action.MINUS });
};
const handlerPlus = async () => {
dispatch({ type: Action.PLUS });
};
return (
<Flex
alignItems="center"
justifyContent="space-between"
borderWidth={1}
borderColor={c}
borderRadius="md"
>
<IconButton
onClick={handlerMinus}
variant="outline"
aria-label="minus"
icon={<HiMinusSm size="20" />}
borderWidth={0}
borderRightWidth={1}
borderRightColor={c}
borderRightRadius="0"
color={c}
_hover={{
background: "white",
color: c
}}
_active={{
background: "white",
color: c
}}
/>
<Box fontFamily="Menlo" fontSize="xl">
{state.value}
</Box>
<IconButton
onClick={handlerPlus}
variant="outline"
aria-label="plus"
icon={<HiPlusSm size="20" />}
borderWidth={0}
borderLeftWidth={1}
borderLeftColor={c}
borderLeftRadius="0"
color={c}
_hover={{
background: "white",
color: c
}}
_active={{
background: "white",
color: c
}}
/>
</Flex>
);
};
const Product = ({ product }: ProductProps) => {
const url = `/images/products/${product.image}`;
const handlerCounter = (value: number) => {
console.log( value);
};
return (
<Flex>
<Box>
<Image
className="image"
loader={productImageloader}
src={url}
alt={product.name}
layout="fill"
objectFit="contain"
/>
</Box>
// other stuff
<Counter handler={handlerCounter} />
</Flex>
);
};
I believe it would be better to surface the reducer state to the top most parent that actually needs to consume it. In this case, that would be the parent (it won't need to go up to the grand-parent, just the highest consumer).
Parent can then pass down the dispatch to the child as noted in the docs: https://reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down

Is it a valid way to write redux actions and reducer?

I've built a cards war game. I'm new to redux and wonder if I use it the correct way, especially when I declare actions in the Game and Main components, and use action's payloads as callbacks to update the state. Also, It feels like a lot of code for a small app. Maybe you can help guys and give me some insights if i'm doing it the wrong way and why, thanks. I put here the relevant components and the full code is here:
https://github.com/morhaham/cards-war-redux
store.js:
import { createStore } from "redux";
const state = {
game_ready: false,
cards: [],
player: { name: "", cards: [], points: 0 },
computer: { name: "computer", cards: [], points: 0 },
};
const reducer = (state, action) => {
switch (action.type) {
case "INIT_GAME_CARDS":
return action.payload(state);
case "UPDATE_PLAYER_NAME":
return action.payload(state);
case "SET_GAME_READY":
return action.payload(state);
case "DIST_CARDS":
return action.payload(state);
case "SET_NEXT_CARDS":
return action.payload(state);
case "INCREASE_POINTS":
return action.payload(state);
case "RESET_GAME":
return action.payload(state);
default:
return state;
}
};
const store = createStore(reducer, state);
export default store;
Main.js:
import React, { useEffect } from "react";
import { Button } from "#material-ui/core";
import { useHistory } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { NUM_OF_CARDS, MAX_CARD_VALUE } from "./constants";
import { shuffle } from "./helpers";
// action creator to initialize the game
const initGameCards = () => ({
type: "INIT_GAME_CARDS",
payload: (state) => {
// creates an array of size 52 filled with 1..13 four times
const cards = Array(NUM_OF_CARDS / MAX_CARD_VALUE)
.fill(
Array(13)
.fill()
.map((_, i) => i + 1)
)
.flat();
// shuffle the cards
shuffle(cards);
return {
...state,
cards,
};
},
});
// action creator to control the player's name
const updatePlayerName = (name) => ({
type: "UPDATE_PLAYER_NAME",
payload: (state) => ({
...state,
player: { ...state.player, name: name },
}),
});
const setGameReady = () => ({
type: "SET_GAME_READY",
payload: (state) => ({
...state,
game_ready: true,
}),
});
function Main() {
const history = useHistory();
const dispatch = useDispatch();
const player = useSelector(({ player }) => player);
// const game_ready = useSelector(({ game_ready }) => game_ready);
const handleClick = React.useCallback(
(e) => {
e.preventDefault();
if (player.name) {
dispatch(setGameReady());
history.replace("./game");
}
},
[dispatch, player.name]
);
useEffect(() => {
dispatch(initGameCards());
}, []);
const handleChange = React.useCallback((e) => {
const target = e.target;
const val = target.value;
switch (target.id) {
case "playerName":
dispatch(updatePlayerName(val));
break;
default:
break;
}
});
return (
<div>
{/* check for valid input */}
<form>
<label htmlFor="playerName">
<h1 className="text-blue-800 text-5xl text-shadow-lg mb-3">
Ready for war
</h1>
</label>
<input
className="border focus:ring-2 focus:outline-none"
id="playerName"
required
onChange={handleChange}
placeholder="Enter your name"
type="text"
value={player.name}
/>
{!player.name ? (
<p className="text-red-700">Please fill the field</p>
) : (
""
)}
<Button
onClick={handleClick}
type="submit"
color="primary"
variant="contained"
>
Start
</Button>
</form>
</div>
);
}
export default Main;
Game.js:
import { Button } from "#material-ui/core";
import React from "react";
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useHistory } from "react-router-dom";
import { NUM_OF_CARDS } from "./constants";
import { shuffle } from "./helpers";
// action creator to distribute the cards at the beginning of the game
const distCards = () => ({
type: "DIST_CARDS",
payload: (state) => {
const cards = [...state.cards];
shuffle(cards);
const computer_cards = cards.slice(0, NUM_OF_CARDS / 2);
const player_cards = cards.slice(NUM_OF_CARDS / 2);
const computer_current_card = computer_cards.pop();
const player_current_card = player_cards.pop();
return {
...state,
cards,
// distributes cards evenly
computer: {
...state.computer,
cards: computer_cards,
current_card: computer_current_card,
points: 0,
},
player: {
...state.player,
cards: player_cards,
current_card: player_current_card,
points: 0,
},
};
},
});
const setNextCards = () => ({
type: "SET_NEXT_CARDS",
payload: (state) => {
let [computer_cards, player_cards] = [
[...state.computer.cards],
[...state.player.cards],
];
const [computer_next_card, player_next_card] = [
computer_cards.pop(),
player_cards.pop(),
];
return {
...state,
player: {
...state.player,
cards: player_cards,
current_card: player_next_card,
},
computer: {
...state.computer,
cards: computer_cards,
current_card: computer_next_card,
},
};
},
});
const pointsIncreament = () => ({
type: "INCREASE_POINTS",
payload: (state) => {
const [player_current_card, computer_current_card] = [
state.player.current_card,
state.computer.current_card,
];
return {
...state,
player: {
...state.player,
points:
player_current_card > computer_current_card
? state.player.points + 1
: state.player.points,
},
computer: {
...state.computer,
points:
player_current_card < computer_current_card
? state.computer.points + 1
: state.computer.points,
},
};
},
});
function Game() {
const player = useSelector(({ player }) => player);
const computer = useSelector(({ computer }) => computer);
const game_ready = useSelector(({ game_ready }) => game_ready);
const dispatch = useDispatch();
const history = useHistory();
const handleReset = React.useCallback(() => {
dispatch(distCards());
}, [dispatch]);
useEffect(() => {
if (game_ready) {
dispatch(distCards());
} else {
history.replace("/");
}
}, [game_ready]);
useEffect(() => {
if (player.current_card && computer.current_card) {
dispatch(pointsIncreament());
}
}, [player.current_card, computer.current_card]);
const handleClick = React.useCallback(() => {
dispatch(setNextCards());
});
return (
<div className="flex justify-center">
<div className="flex flex-col">
<div>
<div>{player.name}</div>
<div>Points: {player.points}</div>
<div>{player.current_card}</div>
</div>
<div>
<div>{computer.current_card}</div>
<div>Points: {computer.points}</div>
<div>{computer.name}</div>
</div>
{!player.cards.length || !computer.cards.length ? (
<Button
onClick={handleReset}
type="submit"
color="primary"
variant="contained"
>
Again?
</Button>
) : (
<Button
onClick={handleClick}
type="submit"
color="primary"
variant="contained"
>
next
</Button>
)}
<div>
{!player.cards.length || !computer.cards.length ? (
player.points === computer.points ? (
<h2>It's a tie</h2>
) : player.points > computer.points ? (
<h2>{player.name} won!</h2>
) : (
<h2>{computer.name} won!</h2>
)
) : (
""
)}
</div>
</div>
</div>
);
}
export default Game;

Update item array react redux

I have a list of items with a checkbox and I need to update the state in redux, just in the 'checked' item.
In this case, when it is 'checked' it is true and when it is not checked it is false.
My reducer code:
const initialState = {
esportes: [
{
externos: [
{
label: 'Futebol de campo',
checked: false,
name: 'futebolcampo',
},
{
label: 'Vôlei de areia',
checked: false,
name: 'voleiareai',
},
],
},
{
internos: [
{
label: 'Vôlei de quadra',
checked: false,
name: 'voleiquadra',
},
{
label: 'Futebol de salão',
checked: false,
name: 'futebosalao',
},
],
},
],
};
const EsportesReducer = (state = initialState, action) => {
switch (action.type) {
case 'UPDATE_ESPORTES':
return {};
default:
return state;
}
};
export default EsportesReducer;
My return page:
import React from 'react';
import {
Grid,
Paper,
Typography,
FormControlLabel,
Checkbox,
} from '#material-ui/core';
import { useSelector, useDispatch } from 'react-redux';
import { Area } from './styled';
const Esportes = () => {
const dispatch = useDispatch();
const esportes = useSelector(state => state.EsportesReducer.esportes);
const handleChangeCheckbox = event => {
const { checked } = event.target;
const { name } = event.target;
const id = parseInt(event.target.id);
dispatch({
type: 'UPDATE_ESPORTES',
payload: checked,
name,
id,
});
};
return (
<Area>
{console.log(esportes)}
<Paper variant="outlined" className="paper">
<Grid container direction="row" justify="center" alignItems="center">
<Typography>Esportes externos</Typography>
{esportes[0].externos.map((i, k) => (
<FormControlLabel
control={
<Checkbox
checked={i.checked}
onChange={handleChangeCheckbox}
name={i.name}
id="0"
color="primary"
/>
}
label={i.label}
/>
))}
<Typography>Esportes internos</Typography>
{esportes[1].internos.map((i, k) => (
<FormControlLabel
control={
<Checkbox
checked={i.checked}
onChange={handleChangeCheckbox}
name={i.name}
id="1"
color="primary"
/>
}
label={i.label}
/>
))}
</Grid>
</Paper>
</Area>
);
};
export default Esportes;
I know that here:
const EsportesReducer = (state = initialState, action) => {
switch (action.type) {
case 'UPDATE_ESPORTES':
return {};
default:
return state;
}
};
on return I need to make a map to get only the item I want to update. I tried in several ways, but I couldn't.
You need to find the item by the passed id in the action.payload.
const EsportesReducer = (state = initialState, action) => {
switch (action.type) {
case 'UPDATE_ESPORTES':
var chosen = state.esportes.find(esporte => esporte.id === action.payload.id)
return {chosen};
default:
return state;
}
};

Making a separated component disappear with useReducer

I'm currently learning the React Hooks feature so I created a small experiment where an invisible(unmounted) box would appear if the button is clicked; if the box is visible and you click on anywhere on the page except the box, the box would disappear. I'm struggling making the box disappear and I don't know what's causing the bug.
Initial state and the reducer:
const initialState = { visible: false };
const reducer = (state, action) => {
switch (action.type) {
case 'show':
return { visible: true };
case 'hide':
return { visible: false };
default:
return state;
}
};
The Box component:
function Box() {
const [state, dispatch] = useReducer(reducer, initialState);
const boxElement = useRef(null);
const boxStyle = {
width: '200px',
height: '200px',
background: 'blue'
};
function hideBox(e) {
if(!boxElement.current.contains(e.target)) {
dispatch({ type: 'hide' });
}
}
useEffect(() => {
window.addEventListener('click', hideBox);
return () => {
window.removeEventListener('click', hideBox);
}
});
return <div style={boxStyle} ref={boxElement} />
}
Main:
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
function showBox() {
dispatch({ type: 'show' });
}
return (
<section>
{ state.visible && <Box /> }
<button onClick={showBox}>Show box</button>
</section>
)
}
You are using two instances of useReducer whereas you only need to have one at the App component level and pass dispatch as a prop to Box otherwise you would only be updating the state that is used by the useReducer in Box and not the state in App component
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
function showBox() {
dispatch({ type: 'show' });
}
return (
<section>
{ state.visible && <Box dispatch={dispatch}/> }
<button onClick={showBox}>Show box</button>
</section>
)
}
Box.js
function Box({dispatch}) {
const boxElement = useRef(null);
const boxStyle = {
width: '200px',
height: '200px',
background: 'blue'
};
function hideBox(e) {
if(!boxElement.current.contains(e.target)) {
dispatch({ type: 'hide' });
}
}
useEffect(() => {
window.addEventListener('click', hideBox);
return () => {
window.removeEventListener('click', hideBox);
}
});
return <div style={boxStyle} ref={boxElement} />
}
Working demo

Resources