Loop in useEffect? - reactjs

I'm trying to select a date and the data should be reloaded every time a specific date is selected. But with my code, it would loop.
Currently I'm using syncfusion's schedule component. Redux toolkit.
Here is my code.
const Schedules = () => {
const { facilities } = useSelector((store) => store.allFacilities);
const { fieldsScheduler } = useSelector((store) => store.allFields);
const { timeSlots, isLoadingReservation } = useSelector(
(store) => store.allReservation
);
const [calRef, setCalRef] = useState();
const [time, setTime] = useState("");
useEffect(() => {
if (calRef && time) {
console.log("TIME", time);
dispatch(getAllReservation(time)); <--- Loop
}
}, [time, calRef]);
useEffect(() => {
dispatch(getFacilities());
dispatch(getFields());
}, []);
const dispatch = useDispatch();
if (isLoadingReservation) {
return <Loading />;
}
const headerInfo = (props) => {
return (
<Grid container>
<Grid item xs={12}>
<h6>{props.subject}</h6>
</Grid>
</Grid>
);
};
const bodyInfo = (props) => {
return (
<Grid container>
<Grid item xs={12}>
<h6>{`Giá tiền cụ thể`}</h6>
</Grid>
</Grid>
);
};
const footerInfo = (props) => {
return (
<Grid container>
<Grid item xs={12}>
<h6>{`Footer here`}</h6>
</Grid>
</Grid>
);
};
const eventTemplate = (props) => {
return (
<>
<div>
<Typography variant="p">
{`${props.subject}: ${props.rentalFee}`}K
</Typography>
</div>
<div>
<Typography variant="p">{`${moment(props.startTime, "HHmm").format(
"HH:mm"
)} - ${moment(props.endTime, "HHmm").format("HH:mm")} `}</Typography>
</div>
</>
);
};
const onDataBinding = () => {
// var scheduleObj = document.querySelector(".e-schedule").ej2_instances[0];
// var currentViewDates = scheduleObj.getCurrentViewDates();
// dispatch(setCurrentDate(moment(startDate).format("YYYY-MM-DD[T]HH:mm:ss")));
// dispatch(
// getAllReservation(moment(startDate).format("YYYY-MM-DD[T]HH:mm:ss"))
// );
var currentViewDates = calRef.getCurrentViewDates();
var startDate = currentViewDates[0];
var endDate = currentViewDates[currentViewDates.length - 1];
console.log("Start date", startDate);
// setTime(moment(startDate).format("YYYY-MM-DD[T]HH:mm:ss"));
};
return (
<MDBox
width="100%"
height="100%"
minHeight="100vh"
borderRadius="lg"
shadow="lg"
bgColor="white"
sx={{ overflowX: "hidden" }}
>
<ScheduleComponent
cssClass="timeline-resource-grouping"
width="100%"
height="100%"
locale="vi"
readonly={true}
currentView="TimelineDay"
allowDragAndDrop={false}
dataBinding={onDataBinding}
// onChange={onDataBinding}
ref={(t) => setCalRef(t)}
// quickInfoTemplates={{
// header: headerInfo.bind(this),
// content: bodyInfo.bind(this),
// footer: footerInfo.bind(this),
// }}
//Data get all event in here. then mapping in ResourceDirective (field)
eventSettings={{
dataSource: timeSlots,
fields: {
subject: { name: "subject" },
id: "reservationId",
endTime: { name: "endTime" },
startTime: { name: "startTime" },
rentalFee: { name: "rentalFee", title: "Phí thuê sân" },
},
template: eventTemplate.bind(this),
}}
group={{ resources: ["Facilities", "Fields"] }}
>
<ResourcesDirective>
<ResourceDirective
field="facilityId"
title="Chọn cơ sở"
name="Facilities"
allowMultiple={false}
dataSource={facilities}
textField="facilityName"
idField="id"
></ResourceDirective>
<ResourceDirective
field="fieldId"
title="Sân"
name="Fields"
allowMultiple={true}
dataSource={fieldsScheduler}
textField="fieldName"
idField="id"
groupIDField="facilityId"
colorField="color"
></ResourceDirective>
</ResourcesDirective>
<ViewsDirective>
<ViewDirective option="TimelineDay" />
<ViewDirective option="Day" />
</ViewsDirective>
<Inject
services={[
Day,
Week,
TimelineViews,
TimelineMonth,
Agenda,
Resize,
DragAndDrop,
]}
/>
</ScheduleComponent>
</MDBox>
);
};
export default Schedules;
This is my slice.
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { toast } from "react-toastify";
import customFetch from "utils/axios";
const initialState = {
isLoadingReservation: false,
timeSlots: [],
currentDate: "",
};
const authHeader = (thunkAPI) => {
return {
headers: {
Authorization: `Bearer ${thunkAPI.getState().user.user.accessToken}`,
},
};
};
export const getAllReservation = createAsyncThunk(
"allReservation/getAllReservation",
async (currentDay, thunkAPI) => {
console.log("Log:", currentDay);
try {
const response = await customFetch.post(
`/reservation-slots`,
{ date: currentDay },
authHeader(thunkAPI)
);
return response.data.timeSlots;
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data);
}
}
);
const allReservationSlice = createSlice({
name: "allReservation",
initialState,
reducers: {
setCurrentDate: (state, action) => {
console.log("Payload", action.payload);
state.currentDate = action.payload;
},
},
extraReducers: {
[getAllReservation.pending]: (state, action) => {
state.isLoadingReservation = true;
},
[getAllReservation.fulfilled]: (state, action) => {
state.isLoadingReservation = false;
state.timeSlots = action.payload;
},
[getAllReservation.rejected]: (state, action) => {
state.isLoadingReservation = false;
toast.error(action.payload);
},
},
});
export const { setCurrentDate } = allReservationSlice.actions;
export default allReservationSlice.reducer;
But there is no way to do that. I used useEffect to update the UI again, but it still loop again.
I don't know if there is a way to solve my problem?
I sat for 3 days straight and the situation did not improve much.

Related

Unable to use value of useRef in value of input in react native

I have a dropdown based on which different fields gets rendered. Initially I have initialized selected option of dropdown in useEffect hook, corresponding fields gets rendered and are working fine.
But when different option or even same option is chosen from dropdown fields gets rendered but when I type something in it it gets cleared. I have used useRef to store values for field at index i in fields array.
export default function CreateEntry({ navigation }) {
const { user, setLoading, setLoadingMsg } = useAuthContext();
const [name, setName] = useState("");
const [selectedCategory, setSelectedCategory] = useState("");
const [categoriesList, setCategoriesList] = useState([]);
const [primary, setPrimary] = useState("");
const [primaryValue, setPrimaryValue] = useState("");
const [fields, setFields] = useState([
{
name: "Email",
value: "",
type: "string",
encrypt: false,
},
{
name: "Password",
value: "",
type: "string",
encrypt: true,
},
]);
const refInputs = useRef([]);
useEffect(() => {
firestore()
.collection("categories")
.doc(user.uid)
.get()
.then((documentSnapshot) => {
setCategoriesList(documentSnapshot.data().category);
setSelectedCategory(documentSnapshot.data().category[0].name);
setFields(documentSnapshot.data().category[0].fields);
fields.map((field) => {
refInputs.current.push("");
});
})
.catch((error) => {
console.log(error);
});
}, []);
const addInput = () => {
refInputs.current.push("");
setFields([
...fields,
{ name: "First", value: "", type: "string", encrypt: false },
]);
};
const removeInput = (i) => {
refInputs.current[i] = "";
setFields((fields) =>
fields.map((field, index) => {
if (index === i) {
return null;
}
return field;
})
);
};
const setInputValue = (index, value) => {
refInputs.current[index] = value;
};
const inputs = [];
fields.forEach((field, i) => {
if (field !== null) {
inputs.push(
<View
key={i}
>
<TextInput
onChangeText={(value) => setInputValue(i, value)}
value={refInputs.current[i]}
/>
<TouchableOpacity
onPress={() => removeInput(i)}
>
<Ionicons name="close" style={[tw``]} size={22} color="#ddd" />
</TouchableOpacity>
</View>
);
}
});
const addEntry = () => {
//
};
return (
<ScrollView style={styles.container}>
<Dropdown
data={categoriesList}
value={selectedCategory}
onChange={(item) => {
refInputs.current = [];
setSelectedCategory(item.name);
setFields(item.fields);
item.fields.map((field) => {
refInputs.current.push("");
});
}}
/>
<Input
onChangeText={setName}
value={name}
placeholder="Name"
/>
{inputs}
<Pressable onPress={addInput}>
<Text}>+ Add a new input</Text>
</Pressable>
<MainButton onPress={addEntry}>Add</MainButton>
</ScrollView>
);
}

React not working with memo and tree structure

I've been going at it for 2 days and cannot figure it out :(
I have a tree-like conversation as you can see in the screenshot.
When a person types something in an empty input field, the Message is then added to the reducer array "conversationsData.messages". When that happens, the Replies component of each Message is listening to changes to only the ~3 replies of that message. If the replies change, then the Replies should rerender. Buuut... The problem is that every single Reply component, and thus every single message is being re-rendered which is causing lag.
Can you please help me get the memo to work properly?
ConversationManager/Conversation/Message.tsx
import React, { FunctionComponent, ReactElement, useRef, useState, useEffect, useMemo } from 'react'
import { useDispatch, useStore, useSelector } from 'react-redux'
import Autosuggest, { OnSuggestionSelected, ChangeEvent } from 'react-autosuggest'
import colors from '#common/colors'
import { updateMessage, removeMessage } from '#reducers/conversationsData'
import usePrevious from 'react-hooks-use-previous'
import MessageInterface, { MessageInitState } from '#interfaces/message'
import { RootState } from '#reducers/rootReducer'
import NewReply from './NewReply'
import { StyleSheet } from '#interfaces/common'
interface IMessageProps {
origMessage: MessageInterface,
isSubClone: boolean,
firstRender: boolean, // It's firstRender=true if we're rendering the message for the first time, "false" if it's a dynamic render
isStarter?: boolean
}
const MessageFunc = ({ origMessage, isSubClone, firstRender }: IMessageProps): ReactElement | null => {
if(!origMessage.id){
return null
}
const dispatch = useDispatch()
const store = useStore()
const state: RootState = store.getState()
const [inputSuggestions, setInputSuggestions] = useState<MessageInterface[]>([])
const [inputWidth, setInputWidth] = useState(0)
const $invisibleInput = useRef<HTMLInputElement>(null)
const isFirstRun = useRef(true)
const [localMessage, setLocalMessage] = useState<MessageInterface>(MessageInitState)
const previousLocalMessage = usePrevious<MessageInterface>(localMessage, MessageInitState)
useEffect(() => {
isFirstRun.current = true
setLocalMessage(origMessage)
}, [origMessage])
useEffect(() => {
if(!localMessage.id) return
if(isFirstRun.current == true){
setupInputWidth()
isFirstRun.current = false
}
if(previousLocalMessage.text != localMessage.text){
setupInputWidth()
}
if(previousLocalMessage.cloneId != localMessage.cloneId){
setupIfMessageClone()
}
}, [localMessage])
const characterMessages = state.conversationsData.messages.filter((m) => {
return m.characterId == origMessage.characterId
})
const parent: MessageInterface = characterMessages.find((m) => {
return m.id == origMessage.parentId
}) || MessageInitState
const setupIfMessageClone = () => { // This function is only relevant if this message is a clone of another one
if(!localMessage.cloneId) return
const cloneOf = characterMessages.find((m) => {
return m.id == localMessage.cloneId
}) || MessageInitState
setLocalMessage({
...localMessage,
text: cloneOf.text
})
}
const setupInputWidth = () => {
let width = $invisibleInput.current ? $invisibleInput.current.offsetWidth : 0
width = width + 30 // Let's make the input width a bit bigger
setInputWidth(width)
}
const _onFocus = () => {
// if(!localMessage.text){ // No message text, create a new one
// dispatch(updateMessage(localMessage))
// }
}
const _onBlur = () => {
if(localMessage.text){
dispatch(updateMessage(localMessage))
}
// No message, delete it from reducer
else {
dispatch(removeMessage(localMessage))
}
}
const _onChange = (event: React.FormEvent, { newValue }: ChangeEvent): void => {
setLocalMessage({
...localMessage,
cloneId: '',
text: newValue
})
}
const _suggestionSelected: OnSuggestionSelected<MessageInterface> = (event, { suggestion }) => {
setLocalMessage({
...localMessage,
cloneId: suggestion.id
})
}
const getSuggestions = (value: string): MessageInterface[] => {
const inputVal = value.trim().toLowerCase()
const inputLen = inputVal.length
return inputLen === 0 ? [] : characterMessages.filter(message =>
message.text.toLowerCase().slice(0, inputLen) === inputVal
)
}
if(!localMessage.id){
return null
}
else {
return (
<>
<li>
<div>
<Autosuggest
suggestions={inputSuggestions}
onSuggestionsFetchRequested={({ value }) => setInputSuggestions(getSuggestions(value))}
onSuggestionsClearRequested={() => setInputSuggestions([])}
getSuggestionValue={(suggestion) => suggestion.text}
onSuggestionSelected={_suggestionSelected}
renderSuggestion={(suggestion) => (
<div>
{suggestion.text}
</div>
)}
theme={{ ...autoSuggestTheme, input: {
...styles.input,
width: inputWidth,
borderBottomColor: localMessage.cloneId ? colors.purple : 'default',
borderBottomWidth: localMessage.cloneId ? 2 : 1
} }}
inputProps={{
value: localMessage.text,
onChange: _onChange,
onBlur: _onBlur,
onFocus: _onFocus,
className: 'form-control',
disabled: isSubClone
}}
/>
<span style={styles.invisibleSpan} ref={$invisibleInput}>{localMessage.text}</span>
</div>
<ul className="layer">
<Replies parentMessage={localMessage} isSubClone={isSubClone} />
</ul>
</li>
</>
)
}
}
const Message = React.memo(MessageFunc)
// const Message = MessageFunc
interface IRepliesProps {
parentMessage: MessageInterface,
isSubClone: boolean
}
const RepliesFunc: FunctionComponent<IRepliesProps> = ({
parentMessage, isSubClone
}: IRepliesProps): ReactElement | null => {
const previousParentMessage = usePrevious<MessageInterface>(parentMessage, MessageInitState)
const isFirstRun = useRef(true)
const replies: MessageInterface[] = useSelector((state: RootState) => state.conversationsData.messages.filter((m) => {
// If parent is regular message
if(!parentMessage.cloneId){
return m.parentId == parentMessage.id && m.characterId == parentMessage.characterId
}
// If parent is a clone, then replies need to come from the main clone
// else {
// return m.parentId == parentMessage.cloneId
// }
}))
if(replies.length){
return (
<>
{console.log('rendering Replies...')}
{replies.map((reply) => {
return (
<Message
origMessage={reply}
key={reply.id}
isSubClone={parentMessage.cloneId ? true : isSubClone}
firstRender={true}
/>
)
})}
{parentMessage.text && !parentMessage.cloneId && !isSubClone && (
<NewReply
parentMessage={parentMessage}
/>
)}
</>
)
}
else {
return null
}
}
// const Replies = React.memo(RepliesFunc)
const Replies = RepliesFunc
export default Message
const styles: StyleSheet = {
input: {
width: 0,
padding: 0,
paddingLeft: 10,
lineHeight: 25,
height: 25,
fontSize: 11,
boxShadow: 'none',
minWidth: 22
},
clone: {
borderBottomWidth: 2,
borderBottomColor: colors.purple
},
invisibleSpan: { // This is used for getting text width of input (for dynamic resizing of input fields)
opacity: 0,
position: 'absolute',
left: -9999,
top: -9999,
fontSize: 11
}
}
const autoSuggestTheme: StyleSheet = {
container: {
position: 'relative'
},
inputOpen: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0
},
suggestionsContainer: {
display: 'none'
},
suggestionsContainerOpen: {
display: 'block',
position: 'absolute',
top: 25,
width: '100%',
minWidth: 400,
border: '1px solid #aaa',
backgroundColor: '#fff',
fontWeight: 300,
fontSize: 11,
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
zIndex: 2
},
suggestionsList: {
margin: 0,
padding: 0,
listStyleType: 'none'
},
suggestion: {
cursor: 'pointer',
padding: '5px 10px'
},
suggestionHighlighted: {
backgroundColor: '#ddd'
}
}
reducers/ConversationsData.ts
import { createSlice, PayloadAction } from '#reduxjs/toolkit'
import MessageInterface from '#interfaces/message'
import axios, { AxiosRequestConfig } from 'axios'
import conversationsDataJSON from '#data/conversationsData.json'
import { AppThunk } from '#reducers/store'
import _ from 'lodash'
interface IInitialState {
loaded: boolean,
messages: MessageInterface[]
}
export const initialState: IInitialState = {
loaded: false,
messages: []
}
export const charactersDataSlice = createSlice({
name: 'conversationsData',
initialState,
reducers: {
loadData: (state, action: PayloadAction<MessageInterface[]>) => {
return state = {
loaded: true,
messages:action.payload
}
},
add: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
state.messages.push(payload.message)
},
edit: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
const updatedConversations = state.messages.map(message => {
if(message.id == payload.message.id && message.characterId == payload.message.characterId){
return message = {
...payload.message,
text: payload.message.cloneId ? '' : payload.message.text // If there's a cloneId, don't save the text since the text comes from the clone parent
}
}
else {
return message
}
})
state.messages = updatedConversations
},
remove: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
_.remove(state.messages, (message) => {
return message.id == payload.message.id && message.characterId == payload.message.characterId
})
}
}
})
const { actions, reducer } = charactersDataSlice
const { loadData, edit, add, remove } = actions
// Thunk actions
// ---------
const loadConversationsData = (): AppThunk => {
return dispatch => {
const conversationsData: MessageInterface[] = conversationsDataJSON
dispatch(loadData(conversationsData))
}
}
const updateMessage = (message: MessageInterface): AppThunk => {
return (dispatch, getState) => {
const existingMessage: MessageInterface | undefined = getState().conversationsData.messages.find((m: MessageInterface) => {
return m.id == message.id && m.characterId == message.characterId
})
// If message exists, update it
if(existingMessage){
dispatch(edit({
message: message
}))
}
// else create a new message
else {
dispatch(add({
message: message
}))
}
setTimeout(() => {
dispatch(saveConversationsData())
}, 10)
}
}
const removeMessage = (message: MessageInterface): AppThunk => {
return (dispatch, getState) => {
const children: MessageInterface[] | [] = getState().conversationsData.messages.filter((m: MessageInterface) => {
return m.parentId == message.id && m.characterId == message.characterId
})
const hasChildren = children.length > 0
// If message has children, stop
if(hasChildren){
alert('This message has children. Will not kill this message. Remove the children first.')
}
// Otherwise, go ahead and kill message
else {
dispatch(remove({
message: message
}))
setTimeout(() => {
dispatch(saveConversationsData())
}, 10)
}
}
}
export const saveConversationsData = (): AppThunk => {
return (dispatch, getState) => {
const conversationsMessages = getState().conversationsData.messages
const conversationsMessagesJSON = JSON.stringify(conversationsMessages, null, '\t')
const options: AxiosRequestConfig = {
method: 'POST',
url: 'http://localhost:8888/api/update-conversations.php',
headers: { 'content-type': 'application/json; charset=UTF-8' },
data: conversationsMessagesJSON
}
axios(options)
.catch(error => console.error('Saving conversationsData error:', error))
}
}
// Exporting it all
// ---------
export { loadConversationsData, updateMessage, removeMessage }
export default reducer
interfaces/message.ts
export default interface MessageInterface {
id: string,
characterId: string,
text: string,
cloneId: string,
parentId: string
}
export const MessageInitState: MessageInterface = {
id: '',
characterId: '',
text: '',
cloneId: '',
parentId: ''
}
Because your selector uses Array.prototype.filter you create a new array every time the messages array changes for each component.
If you would store the data in the state as nested data you can prevent this from happening. For example: {id:1, message:'hello', replies:[{id:2, message:'world', replies:[]}]}.
A simpler way is to use the memoization of reselect to see if each element in the filtered array is the same as it was last time. This will require more resources than the nested solution as it will perform the filter on every change for every branch but won't re render needlessly.
Here is the simple example:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector, defaultMemoize } = Reselect;
const initialState = { messages: [] };
//action types
const ADD = 'ADD';
//helper crating id for messages
const id = ((id) => () => ++id)(0);
//action creators
const add = (parentId, message) => ({
type: ADD,
payload: { parentId, message, id: id() },
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
const { parentId, message, id } = payload;
return {
...state,
messages: state.messages.concat({
id,
parentId,
message,
}),
};
}
return state;
};
//selectors
const selectMessages = (state) => state.messages;
//curry creating selector function that closes over message id
// https://github.com/amsterdamharu/selectors
const createSelectMessageById = (messageId) =>
createSelector([selectMessages], (messages) =>
messages.find(({ id }) => id === messageId)
);
//used to check each item in the array is same as last
// time the function was called
const createMemoizeArray = (array) => {
const memArray = defaultMemoize((...array) => array);
return (array) => memArray.apply(null, array);
};
//curry creating selector function that closes over parentId
// https://github.com/amsterdamharu/selectors
const createSelectMessagesByParentId = (parentId) => {
//memoizedArray([1,2,3]) === memoizedArray([1,2,3]) is true
//https://github.com/reduxjs/reselect/issues/451#issuecomment-637521511
const memoizedArray = createMemoizeArray();
return createSelector([selectMessages], (messages) =>
memoizedArray(
messages.filter((m) => m.parentId === parentId)
)
);
};
//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 AddMessage = ({ addMessage }) => {
const [reply, setReply] = React.useState('');
return (
<div>
<label>
message:
<input
type="text"
onChange={(e) => setReply(e.target.value)}
value={reply}
/>
</label>
<button onClick={() => addMessage(reply)}>Add</button>
</div>
);
};
const AddMessageContainer = React.memo(
function AddMessageContainer({ messageId }) {
const dispatch = useDispatch();
const addMessage = React.useCallback(
(message) => dispatch(add(messageId, message)),
//dispatch in deps should not be needed but
// my linter still complains about it
[dispatch, messageId]
);
return <AddMessage addMessage={addMessage} />;
}
);
const Message = ({ message, replies }) => {
console.log('in message render', message && message.message);
return (
<div>
{message ? <h1>{message.message}</h1> : ''}
{Boolean(replies.length) && (
<ul>
{replies.map(({ id }) => (
<MessageContainer key={id} messageId={id} />
))}
</ul>
)}
{/* too bad optional chaining (message?.id) does not work on SO */}
<AddMessageContainer
messageId={message && message.id}
/>
</div>
);
};
const MessageContainer = React.memo(
function MessageContainer({ messageId }) {
const selectMessage = React.useMemo(
() => createSelectMessageById(messageId),
[messageId]
);
const selectReplies = React.useMemo(
() => createSelectMessagesByParentId(messageId),
[messageId]
);
const message = useSelector(selectMessage);
const replies = useSelector(selectReplies);
return <Message message={message} replies={replies} />;
}
);
const App = () => {
return <MessageContainer />;
};
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>
<div id="root"></div>

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;

Unable to update nested state react

I have a reusable drop down menu component and i render it twice with two different lists and it should update the state with the id of the first element.
the first drop down of the layout update the state without any issue but the second one does not(i switched the order and it always seems the first one updates the state the second doesn't).
please see code
dashbord
const initializeData = {
actionStatuses: [],
actionCategories: [],
actionGroups: [],
actionEvents: [],
actionEventsWithFilter: [],
selectedFilters: {actionStatusId: "", actionCategoryId:""},
};
const Dashboard = ({ selectedPracticeAndFy }) => {
const [data, setData] = useState(initializeData);
const getSelectedStatus = ({ key }) => {
const actionStatusId = key;
const selectedFilters = { ...data.selectedFilters, actionStatusId };
setData((prevState) => {
return { ...prevState, selectedFilters }
});
};
const getSelectedCategory = ({ key }) => {
const actionCategoryId = key;
const selectedFilters = { ...data.selectedFilters, actionCategoryId };
setData((prevState) => {
return { ...prevState, selectedFilters }
});
};
}
result filter:
const ResultFilter = ({actionStatuses, actionCategories, getSelectedStatus, getSelectedCategory}) => {
return (
<Grid
justify="flex-start"
container
>
<Grid item >
<Typography component="div" style={{padding:"3px 9px 0px 0px"}}>
<Box fontWeight="fontWeightBold" m={1}>
Result Filter:
</Box>
</Typography>
</Grid>
<Grid >
<DropdownList payload={actionCategories} onChange={getSelectedCategory} widthSize= {dropdownStyle.medium}/>
<DropdownList payload={actionStatuses} onChange={getSelectedStatus} widthSize= {dropdownStyle.medium}/>
</Grid>
</Grid>
);
}
DropdownList:
const DropdownList = ({ label, payload, onChange, widthSize, heightSize, withBorders, initialData }) => {
const { selectedData, setSelectedData, handelInputChange } = useForm(
payload
);
useEffect(() => {
if (Object.entries(selectedData).length === 0 && payload.length !== 0) {
setSelectedData(payload[0]);
}
}, [payload]);
useEffect(() => {
if (Object.entries(selectedData).length !== 0) {
onChange(selectedData);
}
}, [selectedData]);
return (
<div style={widthSize}>
<div className="ct-select-group ct-js-select-group" style={heightSize}>
<select
className="ct-select ct-js-select"
id={label}
value={JSON.stringify(selectedData)}
onChange={handelInputChange}
style={withBorders}
>
{label && <option value={JSON.stringify({key: "", value: ""})}>{label}</option>}
{payload.map((item, i) => (
<option key={i} value={JSON.stringify(item)} title={item.value}>
{item.value}
</option>
))}
</select>
</div>
</div>
);
};
It may be a stale closure issue, could you try the following:
const getSelectedStatus = ({ key }) => {
setData((data) => {
const actionStatusId = key;
const selectedFilters = {
...data.selectedFilters,
actionStatusId,
};
return { ...data, selectedFilters };
});
};
const getSelectedCategory = ({ key }) => {
setData((data) => {
const actionCategoryId = key;
const selectedFilters = {
...data.selectedFilters,
actionCategoryId,
};
return { ...data, selectedFilters };
});
};

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;
}
};

Resources