How Should I Dispatch Actions When Using React Hooks? - javascript

I'm refactoring a React application to use hooks and have found some success, but I think I am incorrectly dispatching actions (using useReducer) within my application. I've discovered that the state doesn't change immediately when using hooks, and it's causing my application to behave differently than it's class-based counterpart. Here's a piece of my code that dispatches state in which I used console.logs to see if the state has changed or not. In both instances where state.gameOver is logged to the console, it's false, as well as state.userIsWrong. I've attached their actions my reducer as well. Thank you for the help in advance!
const wrongAnswer = () => {
dispatch(toggleGameOver());
console.log(`game over1 ` + state.gameOver)
console.log('wrong answer')
sounds["wrong"].play();
//a delay is used to so that the header will return back to "Click me to begin game" or the current level of the game
//and to return the background to normal
setTimeout(() => {
dispatch(toggleGameOver());
console.log(`game over2` + state.gameOver)
dispatch(turnOnUserIsWrong());
},500)
}
turnOnUserIsWrong action from action.js
export const turnOnUserIsWrong = () => ({
type: 'TURN_ON_USER_IS_WRONG'
})
reducer.js
export default (state, action) => {
switch (action.type) {
case 'SET_ACTIVE_STYLE':
return {
...state,
activeStyle: action.style
}
case 'UPDATE_LAST_COLOR':
return {
...state,
lastColor: action.color
}
case 'UPDATE_USER_PATTERN':
return {
...state,
userPattern: [...state.userPattern, action.id]
}
case 'UPDATE_GAME_PATTERN':
return {
...state,
gamePattern: [...action.newGamePattern]
}
case 'TOGGLE_PRESSED':
console.log(action.color)
return {
...state,
pressed: action.color
}
case 'TURN_ON_READY_FOR_USER_INPUT':
console.log(`here`)
return {
...state,
readyForUserInput: true
}
case 'TURN_OFF_READY_FOR_USER_INPUT':
return {
...state,
readyForUserInput: false
}
case 'RESET_GAME':
return {
...state,
gamePattern: [],
userPattern: [],
lastColor: "",
level: 0,
gameStarted: false,
userIsWrong: false,
readyForUserInput: false,
activeStyle: '',
strictRestart: false
}
case 'UPDATE_LEVEL':
return {
...state,
level: state.level + action.level
}
case 'TURN_OFF_USER_IS_WRONG':
return{
...state,
userIsWrong: false
}
case 'TURN_ON_USER_IS_WRONG':
return{
...state,
userIsWrong: true
}
case 'TOGGLE_STRICT_MODE':
return {
...state,
strictMode: !state.strictMode
}
case 'TOGGLE_GAME_STARTED':
return {
...state,
gameStarted: !state.gameStarted
}
case 'TOGGLE_GAME_OVER':
return {
...state,
gameOver: !state.gameOver
}
case 'EMPTY_USER_PATTERN':
return {
...state,
userPattern: []
}
case 'SET_PLAYER_LEVEL':
return{
...state,
level: action.level
}
default:
return {
...state
};
}
}

Not sure how you're retrieving your state using hooks, but I'm currently working on a React App using only hooks, I'll leave you an example that I hope it helps you:
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
//YOUR OTHER IMPORTS
const YOURCOMPONENTNAME = (props) => {
const gameOver = useSelector((state) => state.YOURREDUCERNAME.gameOver);
const dispatch = useDispatch();
const onToggleGameOver = () => dispatch(toggleGameOver());
const onTurnOnUserIsWrong = () => dispatch(turnOnUserIsWrong());
const wrongAnswer = () => {
onToggleGameOver();
console.log(`game over1 ` + gameOver)
console.log('wrong answer')
sounds["wrong"].play();
//a delay is used to so that the header will return back to "Click me to begin
//game" or the current level of the game and to return the background to normal
setTimeout(() => {
onToggleGameOver();
console.log(`game over2` + gameOver)
onTurnOnUserIsWrong();
},500)
}
// MORE CODE
}
export default YOURCOMPONENTNAME;
Not sure if this is of any help for you, hope it is. In case it´s not, I hope you find your answer!!

Related

I am getting mutation of state error between dispatches from redux

i am getting the following error:
A state mutation was detected between dispatches, in the path 'notifications.adhans.AsrAdhan'. This may cause incorrect behavior. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)]
Following is my notifications reducer in notifications.ts file.
import {
RESTORE_NOTIFICATIONS,
SET_ADHAN,
SET_ADHANTOGGLE,
SET_REMINDER,
SET_REMINDERTOGGLE,
} from "./../constants";
export const initialState = {
reminderToggle: true,
adhanToggle: true,
adhans: {
FajrAdhan: false,
DhuhrAdhan: true,
AsrAdhan: false, //--->getting mutation error for this
MaghribAdhan: true,
IshaAdhan: true,
},
reminders: {
FajrReminder: false,
DhuhrReminder: false,
AsrReminder: true,
MaghribReminder: false,
IshaReminder: true,
SunriseReminder: true,
MidnightReminder: false,
},
};
export default (state = initialState, action: AnyAction): notificationsT => {
switch (action.type) {
case SET_REMINDERTOGGLE:
return {
...state,
reminderToggle: action.payload,
};
case SET_ADHANTOGGLE:
return {
...state,
adhanToggle: action.payload,
};
case SET_ADHAN:
return {
...state,
adhans: {
...state.adhans,
[action.payload.key]: action.payload.value,
},
};
case SET_REMINDER:
return {
...state,
reminders: {
...state.reminders,
[action.payload.key]: action.payload.value,
},
};
case RESTORE_NOTIFICATIONS:
return { ...action.payload };
default:
return state;
}
};
error stack points at src\screens\Home.tsx:127:14 in RestoreState.
Following is the relevant code for Home.tsx
import AsyncStorage from "#react-native-async-storage/async-storage";
import * as actionTypes from "../store/constants";
import { initialState as notifInitState } from "../store/reducers/notifications";
...
const updatedNotificationsstate = { ...notifInitState };
const adhanKeys = Object.keys(notifInitState.adhans);
const keysArray = [
...adhanKeys,
...//other state keys
];
const Home = ({ navigation }) => {
const dispatch = useAppDispatch();
//When component mounts on start, Restore the redux state
React.useEffect(() => {
const RestoreState = async () => {
const savedState = await AsyncStorage.multiGet(keysArray);
savedState.forEach(([key, value]) => {
...
if(value){
//notifications
if (adhanKeys.includes(key))
return (updatedNotificationsstate.adhans[key] = JSON.parse(value));
...
}
});
dispatch({
type: actionTypes.RESTORE_NOTIFICATIONS,
payload: updatedNotificationsstate,
});
};
RestoreState();
}, []);
---
return (
<Layout>
..
</Layout>
);
};
export default Home;
what am i doing wrong here???
Please note I have removed typesrcipt types for javascript coders convenience.
updatedNotificationsstate.adhans[key] = JSON.parse(value) looks to be a mutation.
You've correctly created a shallow copy of updatedNotificationsstate but all the elements are still references to the originals in state, i.e. updatedNotificationsstate.adhans is a reference to the previous state. You must also shallow copy any nested state properties that are being updated.
if (adhanKeys.includes(key)) {
return (updatedNotificationsstate.adhans = { // <-- new object reference
...updatedNotificationsstate.adhans, // <-- shallow copy previous
[key]: JSON.parse(value) // <-- update property
});
}

Redux: altering different parts of the initial state in Reducer according to Actions

I have the following Reducer:
const initialState = {}
const dishReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DISHES':
return (action.dishes)
case 'LOAD_DISHES_ERROR':
console.log("load dishes error")
return state
case 'LOAD_DISHES_SUCCESS':
console.log("load dishes success")
return state
default:
return state;
}
};
export default dishReducer;
And the following action(s):
import {database} from '../../config/fbConfig'
export const startLoadingDishes = (dishes) => {
return (dispatch) =>{
return database.ref('products-dishes').once('value').then((snapshot) => {
let dishes = {}
snapshot.forEach((childSnapshot) => {
let parentkey = childSnapshot.key
let dishArray = [];
childSnapshot.forEach((dish) =>{
dishArray.push(dish.val())
});
dishes[childSnapshot.key] = dishArray;
})
dispatch(loadDishes(dishes))
}).then(() => {
dispatch({ type: 'LOAD_DISHES_SUCCESS' });
}).catch(err => {
dispatch({ type: 'LOAD_DISHES_ERROR' }, err);
});
}
}
export const loadDishes = (dishes) => {
return {
type: 'LOAD_DISHES',
dishes
}
}
The 'startLoadingDishes' action is called inside the componentDidLoad() of a certain Component. However, I want to alter the initial state of my dishReducer so that it includes additional information, as follows:
const initialState = {
value : {},
loaded: false,
loading: false,
error: false
}
So now 'action.dishes' returned by reducer [in 'LOAD_DISHES' case] should be put inside the 'value' part of the state, instead of it being the whole state. Also, the 'loaded' part of the state should be set to true if dishes have already been loaded earlier, and so on. I understand this is fairly simple but as I am new to React+Redux, I don't know how to alter the Action/Reducer codes properly (while keeping state immutability). Any help is appreciated.
I originally asked the question, here is how I solved it (not sure if this is the 'best' way though):
New reducer file:
const initialState = {
value : {},
loaded: false,
loading: false,
error: false
}
const dishReducer = (state = initialState, action) => {
switch (action.type) {
case 'LOAD_DISHES':
return {
value: action.dishes,
loading: !state.loading,
loaded: false, //this might need to be set to true
error: false
}
case 'LOAD_DISHES_ERROR':
console.log("load dishes error")
return {
...state, //or: state.value, as the other parts of state are being overwritten below
loaded: false,
loading: false,
error: true
}
case 'LOAD_DISHES_SUCCESS':
console.log("load dishes success")
return {
...state, //better: state.value
loaded: true,
loading: false,
error: false
}
default:
return state;
}
};
export default dishReducer;
No change in actions file.
Now, inside the 'Main' component, I was originally accessing the state as such:
class Main extends Component {
componentDidMount() {
this.props.startLoadingDishes();
}
render() {
return (
//some code
)
}
}
const mapStateToProps = (state) => {
return {
dishes: state.dishes //to access dishes: dishes.value
}
}
export default connect(mapStateToProps, actions)(Main)
The Main component code also stayed the same, with the difference that now I use 'dishes.value' instead of just 'dishes' to access the value of dishes from the state (and dishes.loaded for loaded, and so on). And now the action caller inside componentDidMount is as follows:
componentDidMount() {
if(!this.props.dishes.loaded){
this.props.startLoadingDishes();
console.log("loading dishes from database")
}
}

returning nested state with redux and thunk

I'm fairly new to redux & thunk, and have been following tutorials to try and understand, and am managing to work it into my app ok. One thing i'm not understanding, is how i can get several state objects on the root level into one nested object. For example, right now my state looks like:
{
timeline: [Array] // My timeline data in an array of objects
timelineHasErrored: false,
timelineIsLoading: false
}
But what I really want is:
{
timeline : {
data: [Array] // My timeline data in an array of objects
hasErrored: false,
isLoading: false
}
}
and i'm really not quite sure how to nest these, or what the proper way to do that is. Below is my redux code, it's pretty simple so i'll post it all.
Reducers index
import { combineReducers } from 'redux'
import { timeline, timelineHasErrored, timelineIsLoading } from './timeline'
export default combineReducers({
timeline, timelineHasErrored, timelineIsLoading
});
Timeline Reducers
import { TIMELINE_HAS_ERRORED, TIMELINE_IS_LOADING, TIMELINE_FETCH_DATA_SUCCESS } from '../constants/action-types.js'
export function timelineHasErrored(state = false, action) {
switch (action.type) {
case TIMELINE_HAS_ERRORED:
return action.hasErrored;
default:
return state;
}
}
export function timelineIsLoading(state = false, action) {
switch (action.type) {
case TIMELINE_IS_LOADING:
return action.isLoading;
default:
return state;
}
}
export function timeline(state = [], action) {
switch (action.type) {
case TIMELINE_FETCH_DATA_SUCCESS:
return action.timeline;
default:
return state;
}
}
Actions
import { TIMELINE_HAS_ERRORED, TIMELINE_IS_LOADING, TIMELINE_FETCH_DATA_SUCCESS } from '../constants/action-types.js'
import api from '../services/api'
export function timelineHasErrored(bool) {
return {
type : TIMELINE_HAS_ERRORED,
hasErrored : bool
}
}
export function timelineIsLoading(bool) {
return {
type : TIMELINE_IS_LOADING,
isLoading : bool
}
}
export function timelineFetchDataSuccess(timeline) {
return {
type : TIMELINE_FETCH_DATA_SUCCESS,
timeline
}
}
export function timelineFetchData() {
return dispatch => {
dispatch( timelineIsLoading(true) )
api.getTracks().then(
res => {
dispatch( timelineIsLoading(false) )
dispatch( timelineFetchDataSuccess(res.body) )
},
err => {
dispatch( timelineIsLoading(false) )
dispatch( timelineHasErrored(true) )
}
)
}
}
And then in my react component I format the object like how i want it... but i think it would be better to have it nested in the actual state so i'm not creating extra work for myself if things change
// Redux State
const mapStateToProps = (state) => {
const obj = {
timeline : {
data : state.timeline,
hasErrored: state.tracksHasErrored,
isLoading: state.tracksIsLoading
}
}
return obj
}
// Redux Dispatch
const mapDispatchToProps = (dispatch) => {
return {
fetchData: () => dispatch( timelineFetchData() )
}
}
If anybody has any tips or corrections for me bring em on, i'm trying to get a solid grasp on redux, thanks!
Your timeline reducer is pretty small, so you could have it as a single reducer as follows:
const initialState = {
data: [],
hasErrored: false,
isLoading: false
};
export function timeline(state = initialState, action) {
switch (action.type) {
case TIMELINE_HAS_ERRORED:
return {
...state,
hasErrored: action.hasErrored
};
case TIMELINE_IS_LOADING:
return {
...state,
isLoading: action.isLoading
};
case TIMELINE_FETCH_DATA_SUCCESS:
return {
...state,
data: action.timeline
};
default:
return state;
}
}
Then you wouldn't need to call combineReducers(), unless you had other reducers.

loading status change after fetch data completely

I have action creator to get data from API and have another action creator for loading status and want change loading status when data completely fetched.
Now, I wrote following codes but not working good, Loading status changes to false before data fetched completely.
My ActionCreator:
export const loadingStatus = (bool) => {
return {
type: Constants.LOADING_STATUS,
isLoading: bool
};
}
const allFlashCards = (action) => {
return{
type: Constants.ALL_CARD,
...action
}
};
export const fetchAllFlashCards = () => {
return (dispatch) => {
dispatch(loadingStatus(true));
return axios.post(API.DISPLAY_ALL_CARDS)
.then((data)=>{
console.warn(data);
dispatch(allFlashCards(data));
dispatch(loadingStatus(false));
}).catch((error)=>{
console.warn(error)
});
}
};
and my Reducer:
const FlashCard = (state = [], action) => {
switch (action.type) {
case Constants.ADD_CARD:
return {...state, data: action.data};
break;
case Constants.ALL_CARD:
return {...state, data: action};
break;
default:
return state;
}
};
export const Loading = (status= false, action) => {
switch (action.type) {
case Constants.LOADING_STATUS:
return action.isLoading;
break;
default:
return status;
}
}
and in my component:
componentDidMount() {
this.props.fetchCards();
}
render() {
return(
<div>
{this.props.loading ?
<Loading/> :
Object.keys(this.props.cards.data).map(this.renderCard)
}
</div>
);
}
const mapStateToProps = (state) => ({
cards: state.main,
loading: state.loading
});
const mapDispatchToProps = (dispatch) => ({
fetchCards: bindActionCreators(fetchAllFlashCards, dispatch)
});
and combineReducer is:
import { combineReducers } from 'redux';
import FlashCard , { Loading } from './FlashCard.js';
import { routerReducer } from "react-router-redux";
export default combineReducers({
main: FlashCard,
loading: Loading,
routing: routerReducer
});
In my page, I have an error in console and it's:
Uncaught TypeError: Cannot read property 'data' of undefined and if put my codes in timeout fixed my bug :/
What should i do?
Your default state is wrong here:
const FlashCard = (state = [], action) => {
switch (action.type) {
case Constants.ADD_CARD:
return {...state, data: action.data};
break;
case Constants.ALL_CARD:
return {...state, data: action};
break;
default:
return state;
}
};
It should be an empty object {} instead of an empty array [], since you're returning objects.
This code
export const fetchAllFlashCards = () => {
return (dispatch) => {
dispatch(loadingStatus(true));
return axios.post(API.DISPLAY_ALL_CARDS)
.then((data)=>{
console.warn(data);
dispatch(allFlashCards(data));
dispatch(loadingStatus(false));
}).catch((error)=>{
console.warn(error)
});
}
};
Looks completely fine. loadingStatus(false) should not be called before setting the flash cards. Your reducers and action creators are synchronous (as they should). So, nothing of note there.
I saw that you're using action.data on the Constants.ADD_CARD action case, but in your code you do not dispatch any actions with that type. Do you do it somewhere else? Maybe that's where the error is?
EDIT:
Another place that you're using .data is in your renderer: this.props.cards.data. What's the value of the state.main?
How are you creating your rootReducer? It should be something like this:
const rootReducer = combineReducers({
main: FlashCard,
loading: Loading,
});
Are you using main there? Or maybe cards?
Finally, I fixed my problem:
In my actionCreator change fetchAllFlashCards method to following:
export const fetchAllFlashCards = () => {
return (dispatch) => {
dispatch(loadingStatus(true));
return axios.post(API.DISPLAY_ALL_CARDS)
.then(({data})=>{
dispatch(allFlashCards(data));
dispatch(loadingStatus(false));
}).catch((error)=>{
console.warn(error)
});
}
};
and in reducer change FlashCard reducer to following:
const FlashCard = (state = [], action) => {
switch (action.type) {
case Constants.ADD_CARD:
return {...state, data: action.data};
break;
case Constants.ALL_CARD:
return {...state, data: action.data};
break;
default:
return state;
}
};

Write action and reducer efficient and clean (react redux)

I have below actions and reducer:
actions:
import { OPEN_NODE, CLOSE_NODE, GET_NODES } from '../constants/NodeActionTypes';
export function openNode(path) {
return {
type: OPEN_NODE,
path: path
};
}
export function closeNode() {
return {
type: CLOSE_NODE
};
}
export function getNodes(path) {
return {
type: GET_NODES,
path: path
};
}
reducer:
export default function opener(state = initialState, action) {
switch (action.type) {
case OPEN_NODE:
var { path } = action
var {nodes} = getFileList(path)
return {
...state,
open:true,
nodes:nodes
};
case CLOSE_NODE:
return {
...state,
open:false
};
case GET_NODES:
var { path } = action
var {nodes} = getFileList(path)
return {
...state,
nodes:nodes
};
default:
return state;
}
}
Obviously, OPEN_NODE contain GET_NODES (only plus open:true), but there seems many way to organize the code:
pack GET_NODES reducer to a function, call this in OPEN_NODE , and add open:true.
modify openNode action, send [OPEN_NODE, GET_NODES] together , but how to write switch(action.type)'s case ?
let OPEN_NODE reducer dispatch a getNodes action to trigger GET_NODES reducer
which is best ? Or any another better way?
You don't have to keep everything inside your switch statement. If you have 2 similar actions, just refactor into a private function and call it.
In your case, it might be something like:
// your reducer helper
const getNodes = (state) => {
var { path } = action
var {nodes} = getFileList(path)
return {
...state,
nodes:nodes
};
};
// your reducer function
export default function opener(state = initialState, action) {
switch (action.type) {
case OPEN_NODE:
return { ...getNodes(state), open:true };
case GET_NODES:
return getNodes(state);
// ....
}
You can simply use the switch statement to execute both actions :
export default function opener(state = initialState, action) {
switch (action.type) {
case OPEN_NODE:
case GET_NODES:
var { path } = action
var {nodes} = getFileList(path)
return {
...state,
nodes:nodes
open: action.type === OPEN_NODE ? true : state.open
};
case CLOSE_NODE:
return {
...state,
open:false
};
default:
return state;
}
}
Check out my github project for creating generic reducers. The solution I purpose will address many of the concerns you currently have.
Redux-Reducer-Generator

Categories

Resources