React component won't update react-redux - reactjs

I have a navbar component containing a dropdown which allows user to change their "region" and it updates the redux store. The update to redux store is working properly. However, it is not re-rendering the parent component.
From this navbar component I use the changeRegion ActionCreator to update the store. I can see the update in localstorage and can even console.log it and see the proper region after its changed. What is NOT happening, is the table in the "UserManagement" component is not updating.
I use setPageData in useEffect() in UserManagement.js to determine what state the table is in. Basically, if there is no region selected (initialState is blank) then it should show an empty table with a dropdown to select region. Once a region is selected, it should then display the data in table.
Clearly I am just missing something, but I have been trying to make this work for way too long and really could use some help.
UserManagement.js
import React, { useState, useEffect } from "react";
import {
Box,
ButtonDropdown,
Table,
} from "private-compontent";
import fakeData from "../fakeData"
import { useSelector, useDispatch } from 'react-redux'
import { bindActionCreators } from 'redux'
import { actionCreators } from '../../../state/actionsCreators'
export const DetailsTable = (props) => {
const [pageData, setPageData] = useState([]);
const region = useSelector((state) => state.region)
const dispatch = useDispatch();
const { changeRegion } = bindActionCreators(actionCreators, dispatch)
const [regionState, setRegionState] = useState(region);
const currentUserRegion = region === '' ? "Select Region" : "Region: " + region;
useEffect(() => {
if (region && region !== '') {
setRegionState(region)
setPageData(fakeData);
} else {
setPageData([{}]);
}
}, []);
// When a new region is selected, save it to localStorage and setRegion
const regionChange = (event) => {
changeRegion(event.detail.id)
}
function EmptyState({ title, subtitle, action }) {
return (
<Box textAlign="center" color="inherit">
<Box variant="strong" textAlign="center" color="inherit">
{title}
</Box>
<Box variant="p" padding={{ bottom: 's' }} color="inherit">
{subtitle}
</Box>
{action}
</Box>
);
}
return (
<>
<Table
{...collectionProps}
items={ items }
empty={
<EmptyState
title="No Data"
subtitle="No data to display"
action={
<ButtonDropdown
items={[
{ text: "NA", id: "NA", disabled: false },
{ text: "EU", id: "EU", disabled: false },
{ text: "FE", id: "FE", disabled: false },
]}
onItemClick={regionChange}
>
{currentUserRegion}
</ButtonDropdown> }
/>
}
/>
</>
);
};
export default DetailsTable;
Navbar.js
import React from "react";
import { ButtonDropdown} from "../ButtonDropdown";
import { useSelector, useDispatch } from 'react-redux'
import { bindActionCreators } from 'redux'
import { actionCreators } from '../../../state/actionsCreators'
export const Navbar = () => {
const region = useSelector((state) => state.region)
const dispatch = useDispatch();
const { changeRegion } = bindActionCreators(actionCreators, dispatch)
// When a new region is selected, save it to localStorage and setRegion
const regionChange = (event) => {
changeRegion(event.detail.id)
}
const currentUserRegion = region === '' ? "Select Region" : "Region: " + region;
// Construct the ButtonDropdown based on the currentPath
const regionButtonDropdown =
// If UserManagement show region options
<ButtonDropdown
items={[
{ text: "NA", id: "NA", disabled: false },
{ text: "EU", id: "EU", disabled: false },
{ text: "FE", id: "FE", disabled: false },
]}
onItemClick={regionChange} // Run regionChange on dropdown change
>
{currentUserRegion}
</ButtonDropdown>
// Return our HTML
return (
<>
<header id="navbar">
{regionButtonDropdown}
</header>
</>
);
};
export default Navbar;
reducers.js
import { combineReducers } from "redux"
import regionReducer from "./regionReducer"
const reducers = combineReducers({
region: regionReducer
})
export default reducers;
regionReducer.js
import * as ACTIONS from '../actionsTypes'
const initialState = '';
const reducer = (state = initialState, action) => {
switch (action.type) {
case ACTIONS.CHANGE_REGION:
return action.payload;
default:
return state;
}
}
export default reducer;
store.js
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import logger from 'redux-logger';
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
import rootReducer from './reducers/reducers'
const persistConfig = {
key: 'root',
storage
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const middleware = applyMiddleware(thunk, logger);
const store = createStore(
persistedReducer,
middleware
);
const persistor = persistStore(store);
export { store, persistor };

Your useMemo only runs on page load. Change it to
useEffect(() => {
if (region && region !== '') {
setRegionState(region)
setPageData(fakeData);
} else {
setPageData([{}]);
}
}, [region]);
So that it runs whenever region changes. See Conditionally firing an effect from the React docs.
Specifically note,
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.

Related

React Redux Useselector Hook Not re-render after store state update

I want to remove an item from an array in redux store, which served as a history array. But the state change does not always trigger re-render. I have to click 4 times delete button, or change tab in order to see state change to trigger re-render.
I have tried the following:
using shallowEqual as comparison function
using Object.assign for a new array object, to trigger state change
But, still no luck. Anyone please guide me on this. My codes are below:
Action:
export function removeHistoryPlaces(index) {
return {
type: type.REMOVE_HISTORY_PLACES,
payload: index,
}
}
store configure
import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from "./reducers/index.js";
import createSagaMiddleware from "redux-saga";
import rootSaga from "./sagas/index";
const sagaMiddleware = createSagaMiddleware();
// TO TEST WITH DEV TOOL
// const store = compose(
// applyMiddleware(sagaMiddleware),
// window.devToolsExtension && window.devToolsExtension(),
// )(createStore)(rootReducer);
const store = createStore(rootReducer,applyMiddleware(sagaMiddleware))
sagaMiddleware.run(rootSaga);
export default store;
reducer
const initialState = {
historyPlaces: [],
}
export default function historyPlaces(state = initialState, action) {
switch (action.type) {
case type.REMOVE_HISTORY_PLACES:
// const newList = Object.assign([],[...state.historyPlaces].filter((place,index)=>index != action.payload))
// const isSame = newList === state.historyPlaces
// console.log("new place list " , newList)
return {
...state,
historyPlaces: state.historyPlaces.filter((place,index)=>index != action.payload),
}
default:
return state
}
}
HistoryList component where user can view, click and delete history
import Chip from "#mui/material/Chip";
import Stack from "#mui/material/Stack";
import { useDispatch } from "react-redux";
import { removeHistoryPlaces } from "../redux/actions/historyPlaces";
export default function HistoryList({ data, onItemClick }) {
const dispatch = useDispatch();
const maxDisplay = 5;
const handleDelete = (index) => {
dispatch(removeHistoryPlaces(index));
};
return (
<Stack direction="row" spacing={1}>
{data.slice(0, maxDisplay).map((item, index) => (
<Chip
style={{maxWidth:'200px'}}
key={new Date().getTime()}
label={item.description}
onClick={()=>onItemClick(item)}
onDelete={()=>handleDelete(index)}
/>
))}
</Stack>
);
}
Home page where HistoryList component is used
import HistoryList from "../components/HistoryList";
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
const libraries = ['places'];
export default function Home() {
const [currentFocusLocation,setCurrentFocusLocation] = useState('')
const places = useSelector(state => state.historyPlaces.historyPlaces,shallowEqual);
const onItemClick = (item) =>{
...
}
useEffect(()=>{
console.log('places update detected')
setCurrentFocusLocation(...)
},[places])
if (!isLoaded) return <div>Loading...</div>;
return (
<>
<div className="home-container">
<HistoryList data={places} onItemClick={onItemClick}/>
</div>
</>
);
}
For complete codes, can check this github link

How i can use useContext hook or just a Context in a middleware in the React-Redux app?

I had faced with a problem. The problem: I had tried to use context inside a middleware, but i`dont know how i can do it, because we can use useContext only in... 'Hooks can only be called inside of the body of a function component. Is it possible to use context inside the middleware? Thx for help!
'I have a context:
import { createContext, useState } from "react";
export const PopupContext = createContext();
export const PopupContextProvider = (props) => {
const [isShow, setIsShow] = useState(false);
return (<PopupContext.Provider
value={[isShow, setIsShow]}
>
{props.children}
</PopupContext.Provider>
)
}
my index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from './redux/store';
import { PopupContextProvider } from './context/popup/popup';
ReactDOM.render(
<React.StrictMode>
<PopupContextProvider>
<Provider store={store}>
<App />
</Provider>
</PopupContextProvider>
</React.StrictMode>,
document.getElementById('root')
);
and my store.js - it`s a Redux store, you know...
import { configureStore } from '#reduxjs/toolkit';
import alertMiddleware from '../middleware/alert.middleware';
import authReducer from './features/auth/auth-slice';
import cardReducer from './features/card/cardSlice';
const store = configureStore({
reducer: {
auth: authReducer,
card: cardReducer
},
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(alertMiddleware)
})
export default store;
and my dumb middleware
import { PopupContext } from '../context/popup/popup';
import { useContext, useState } from 'react';
const alertMiddleware = store => next => action => {
const isShow = useContext(PopupContext);
if (action.type === 'auth/login/rejected') {
console.log(isShow);
}
console.log('middleware');
next(action)
}
export default alertMiddleware;
It's not. Hooks cant be used outside of components. You should send isShow as an action payload when you dispatch the action. Then you would have something like in your middleware
...
if (action.type === 'auth/login/rejected') {
console.log(action.payload.isShow);
}
...
Well, like i had understood, it's imposible to use context in your middleware (so sad). In this way, i had created a slice with reducers and etc. And now, a can take out all my logic in reducers, if someone doesn't know what is it... well, it's like a global state with services, which are available from all points in your application. The logic is: when some action type happen, the middleware handles it and dispatches some actions, in this action you can do everething, in my case i change the state and read this state from my functional component. I belive, what i had done a good explain.
Sequence of actions: some action => middleware => process action in reducer => change state
My middleware:
import { showPopup } from '../features/popup/popup-slice';
const POPUP_PROPERTIES = {
loginRejected: {
message: "LOGIN ERROR",
styles: {
color: "white",
backgroundColor: "red"
}
},
invalidateLoggedInUser: {
message: "You are logged out from your account",
styles: {
color: "white",
backgroundColor: "#4BE066"
}
},
cardCreateFulfilled: {
message: "Card set successfully created",
styles: {
color: "white",
backgroundColor: "#01C9F7"
}
},
cardDeleteFulfilled: {
message: "Card set successfully deleted",
styles: {
color: "white",
backgroundColor: "#4BE066"
}
}
}
const PopupMiddleware = ({ dispatch, getState }) => next => action => {
const { type } = action;
switch (type) {
case 'auth/login/rejected': {
dispatch(showPopup(POPUP_PROPERTIES.loginRejected));
break;
}
case 'auth/invalidateLoggedInUser': {
dispatch(showPopup(POPUP_PROPERTIES.invalidateLoggedInUser));
break;
}
case 'card/create/fulfilled': {
dispatch(showPopup(POPUP_PROPERTIES.cardCreateFulfilled));
break;
}
case 'card/delete/fulfilled': {
dispatch(showPopup(POPUP_PROPERTIES.cardDeleteFulfilled));
break;
}
default: break;
}
next(action);
}
export default PopupMiddleware;
My slice
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
// -------------------------------------- Slice --------------------------------------
const initialState = {
popupEntity: {
message: null,
color: null,
},
isVisible: false
}
const popupSlice = createSlice({
name: 'popup',
initialState,
reducers: {
showPopup: {
reducer(state, action) {
state.popupEntity = action.payload;
state.isVisible = true;
}
},
hidePopup: {
reducer(state) {
state.isVisible = false;
}
}
},
})
export const { showPopup, hidePopup } = popupSlice.actions;
export default popupSlice.reducer;
// -------------------------------------- Selectors --------------------------------------
export const popupStateSelector = state => state.popup;
export const isVisibleSelector = state => state.popup.isVisible;
and store:
import { configureStore } from '#reduxjs/toolkit';
import PopupMiddleware from './middleware/popup.middleware';
import authReducer from './features/auth/auth-slice';
import cardReducer from './features/card/cardSlice';
import popupReducer from './features/popup/popup-slice';
const store = configureStore({
reducer: {
auth: authReducer,
card: cardReducer,
popup: popupReducer
},
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(PopupMiddleware)
})
export default store;

Get new state from redux storage with useSelector returns initial state in React-Native

I have just for several reasons migrated from the old style of redux (pre 2019) which used case, switch etc. The redux store gets updated as it should which i can see in the TextInput for example but when trying to use the selected value from redux store elsewhere inside the screen function it seems to initial state and not the updated one. Shortened code below and any help would be greatly appriciated.
redux/slices/auth.js
import { createSlice } from "#reduxjs/toolkit"
const initialState = {
email: "",
}
const authSlice = createSlice({
name: 'userAuth',
initialState,
reducers: {
setEmail: (state, action) => {
state.email = action.payload.email;
}
}
});
export const { setEmail } = authSlice.actions;
export const selectEmail = (state) => state.userAuth.email;
export default authSlice.reducer;
redux/store.js
import { configureStore } from '#reduxjs/toolkit'
import authSlice from './slices/auth'
export const store = configureStore({
reducer: {
userAuth: authSlice,
},
})
screens/LoginScreen.js
import {useSelector, useDispatch} from 'react-redux';
import { selectEmail, setEmail } from '../../redux/slices/auth';
function LoginScreen({navigation}) {
const _email = useSelector(selectEmail);
const onEmailButtonPress = async () => {
console.log("Begin sign in: Email");
// GETS INITIAL STATE AND NOT UPDATED ONE
if (_email == null || _email == 0 || _email == ""){
console.log(_email);
return;
}
}
return (
<View>
<Text>Login with e-mail</Text>
<TextInput
placeholder="Enter e-mail address"
placeholderTextColor={'#000'}
keyboardType="email-address"
onChangeText={(value) => dispatch(setEmail(value))}
maxLength={128}
value={_email} // SHOWS UPDATED STATE
/>
<TouchableOpacity
onPress={() => onEmailButtonPress()}
>
<Text>Continue</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
});
export default LoginScreen;
App.js
class App extends React.Component {
render() {
LogBox.ignoreAllLogs(true);
return (
<Provider store={store}>
{/*<PersistGate persistor={persistor}>*/}
<LoadingScreen />
{/*</PersistGate>*/}
</Provider>
);
}
}
In the reducer you are reading action.payload.email but in the component you are dispatching setEmail(value).
That's not consistent, or you read in the reducer action.payload or you dispatch in the component setEmail({email: value})

How to use React redux hooks for updating state for just 1 value on click

I'm fairly new to React Redux and am using it to display different transactions on a page. I have set up my store, then created my reducer & selector.
I have a filter state that is controlled by 3 buttons that when clicked, should change the type ('incoming', 'outgoing' or 'trades') and this renders different components based on this change.
Where I seem to be going wrong, is with updating the Redux value on click and ensuring the state also updates, I'm sure its an easy fix but haven't quite figured it out.
This is my reducer:
import {createSlice} from "#reduxjs/toolkit";
export const initialState = {
filter_type: "outgoing",
}
const slice = createSlice({
initialState,
name: "transactionFilter",
reducers: {
setFilterType: (state, {payload}) => {
state.filter_type = payload
}
}
})
const { reducer, actions } = slice;
export const { setFilterType } = actions;
export default reducer;
And my selector:
import {createSelector} from "#reduxjs/toolkit";
const baseState = (state) => state.transaction_filter;
export const transactionFilterSelector = createSelector(
[baseState],
(state) => state.filter_type
)
I'm importing the selector along with useDispatch/useSelector and can see the Redux state in my console.log, how can I use dispatch correctly to update everything?
This is my transaction file:
import React, {useMemo, useEffect, useState} from "react"
import {useDispatch, useSelector} from "react-redux";
const TransactionsPage = (props) => {
const filterState = useSelector(transactionFilterSelector);
console.log('Redux filter state: ', filterState);
const dispatch = useDispatch();
const [filter, setFilter] = useState({
type: filterState,
sources: {
type: "all",
items: []
},
assets: [],
dates: {
type: "all",
from: "",
to: "",
},
category: "all",
})
return (
<div>
<Heading level={'4'}>Choose a type</Heading>
<div className={'tx-type-filter'}>
{Object.values(TxTypes).map((i) => (
<div
className={`tx-type-item ${type === i ? 'selected' : ''}`}
key={i}
onClick={() => {
dispatch({type: setFilterType, payload: i});
}}>
<img src={image(i)} alt={'tx type'} />
</div>
))}
</div>
</div>
)
export default TransactionsPage;
Thank you in advance for any guidance!
You'd do dispatch(setFilterType(i));. You don't write out action objects in modern Redux.

Redux - useSelector returns state as undefined

I'm following a tutorial on learning Redux and I'm stuck at this point where state that should have an image url is returned as undefined.
Image is successfully saved in firbase storage and dispatched but when I try to get the url on new route with useSelector it is undefined.
import React, {useEffect} from "react";
import {useSelector} from "react-redux";
import {useHistory} from "react-router-dom";
import "./ChatView.css";
import {selectSelectedImage} from "./features/appSlice";
function ChatView() {
const selectedImage = useSelector(selectSelectedImage);
const history = useHistory();
useEffect(() => {
if(!selectedImage) {
exit();
}
}, [selectedImage])
const exit = () => {
history.replace('/chats');
}
console.log(selectedImage)
return (
<div className="chatView">
<img src={selectedImage} onClick={exit} alt="" />
</div>
)
}
export default ChatView
reducer created for chat (slice):
import { createSlice } from '#reduxjs/toolkit';
export const appSlice = createSlice({
name: 'app',
initialState: {
user:null,
selectedImage:null,
},
reducers: {
login: (state, action) => {
state.user = action.payload;
},
logout: (state) => {
state.user = null;
},
selectImage:(state, action) => {
state.selectedImage = action.payload
},
resetImage:(state) => {
state.selectedImage = null
}
},
});
export const { login, logout, selectImage, resetImage} = appSlice.actions;
export const selectUser = (state) => state.app.user;
export const selectSelectedImage = (state) => state.app.selectImage;
export default appSlice.reducer;
and code for dispatching that imageURL which when i console.log it gives the correct url:
import {Avatar} from "#material-ui/core";
import StopRoundedIcon from "#material-ui/icons/StopRounded"
import "./Chat.css";
import ReactTimeago from "react-timeago";
import {selectImage} from "./features/appSlice";
import {useDispatch} from "react-redux";
import {db} from "./firebase";
import {useHistory} from "react-router-dom";
function Chat({id, username, timestamp, read, imageUrl, profilePic}) {
const dispatch = useDispatch();
const history = useHistory();
const open = () => {
if(!read) {
dispatch(selectImage(imageUrl));
db.collection('posts').doc(id).set({read:true,}, {merge:true});
history.push('/chats/view');
}
};
return (
<div onClick={open} className="chat">
<Avatar className="chat__avatar" src={profilePic} />
<div className="chat__info">
<h4>{username}</h4>
<p>Tap to view - <ReactTimeago date={new Date(timestamp?.toDate()).toUTCString()} /></p>
</div>
{!read && <StopRoundedIcon className="chat__readIcon" />}
</div>
)
}
export default Chat
Your selector is trying to access the wrong field.
export const selectSelectedImage = (state) => state.app.selectImage;
Should actually be:
export const selectSelectedImage = (state) => state.app.selectedImage;
as your state has selectedImage field and not selectImage.

Resources