React: useContext is not updating current state - reactjs

i'm actually stuck, i'm trying to find a way to centralize data into my app.
When i'm clicking on the button, isDisplay is supposed to be true ; the state is changing in my context file but not into the app.
thx !
Button.tsx
const Button = () => {
const { state, dispatch } = useContext(AppContext);
const { isDisplay } = state;
return (
<Fragment>
<BootstrapButton
onClick={() => {
dispatch({
type: "DISPLAY_USERS",
payload: state.users,
});
}}
variant={isDisplay ? "success" : "primary"}
>
{isDisplay ? "Albums chargés!" : "Charger les albums"}
</BootstrapButton>
</Fragment>
);
};
export default Button;
reducer.ts
import { RawUser } from "../interfaces";
import { InitialStateType } from "./context";
type ActionMap<M extends { [index: string]: any }> = {
[Key in keyof M]: M[Key] extends undefined
? {
type: Key;
}
: {
type: Key;
payload: M[Key];
};
};
type UsersPayload = {
["LOAD_USERS"]: RawUser[];
["DISPLAY_USERS"]: RawUser[];
};
export type LoadUsersActions =
ActionMap<UsersPayload>[keyof ActionMap<UsersPayload>];
export const loadUsersReducer = (
state: InitialStateType,
action: LoadUsersActions
) => {
switch (action.type) {
case "LOAD_USERS":
return {
...state,
users: action.payload,
isLoading: true,
};
case "DISPLAY_USERS":
return {
...state,
isDisplay: true,
};
default:
return state;
}
};
context.tsx
export type InitialStateType = {
users: RawUser[];
isLoading: boolean;
isDisplay: boolean;
};
export const initialState = {
users: [],
isLoading: true,
isDisplay: false,
};
const AppContext = createContext<{
state: InitialStateType;
dispatch: Dispatch<LoadUsersActions>;
}>({
state: initialState,
dispatch: () => null,
});
const mainReducer = (data: InitialStateType, action: LoadUsersActions) => ({
data: loadUsersReducer(data, action),
});
const AppProvider: FC = ({ children }) => {
const [state, dispatch] = useReducer(mainReducer, initialState as never);
useEffect(() => {
axios.get("https://jsonplaceholder.typicode.com/users").then((result) => {
dispatch({ type: "LOAD_USERS", payload: result.data });
});
}, []);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
export { AppProvider, AppContext };
App.tsx
import React, { useContext, useEffect, useState } from "react";
import Jumbotron from "react-bootstrap/Jumbotron";
import Container from "react-bootstrap/Container";
import DefaultButton from "./components/button/Button";
import UserCards from "./components/cards/UserCardsPlaceHolder";
import { AppContext, AppProvider } from "./context/context";
import UsersLoaded from "./components/cards/UsersLoaded";
import { UseGetUsers } from "./api/usersList";
function App() {
const { state } = useContext(AppContext);
const { isDisplay } = state;
console.log(state);
return (
<AppProvider>
<main className="main">
<Jumbotron fluid>
<Container fluid="md">
<h1 className="mb-5">Keep calm, take a deep breath...</h1>
<DefaultButton />
</Container>
</Jumbotron>
<Container fluid="md">
{isDisplay ? <UsersLoaded /> : <UserCards />}
</Container>
</main>
</AppProvider>
);
}
export default App;

It looks like in App.tsx you are accessing an undefined value isDisplay - does it compile?
it should be
const { state: { isDisplay } } = useContext(AppContext);
instead of
const { state } = useContext(AppContext);

Related

when I use useDispatch inside useEffect my component keep rendering

When I use dispatch as follows in my react component, My component keeps rendering. How can I avoid that?
const dispatch = useDispatch();
useEffect(() => {
dispatch(reportsActionCreators.changeSalesDashboardData(someData));
}, []);
in the parent component, I'm using useSelector as this. But didn't use this report's data.
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return {
selectedSalesTab: state.reports.selectedSalesTab,
};
this is the parent component I'm using.
const SalesReports: FC = () => {
const dispatch = useDispatch();
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return {
selectedSalesTab: state.reports.selectedSalesTab,
};
});
const getPageContent = useMemo(() => {
switch (selectedSalesTab) {
case salesReportsTabs[0].id:
return <Dashboard />;
default:
return <div>not found :(</div>;
}
}, [selectedSalesTab]);
return (
<div className="sales-report-wrapper">
<GTTabs
id="sales-reports-tabs"
onClickTab={(tab: Tab) => dispatch(reportsActionCreators.changeSalesTab(tab.id))}
tabs={salesReportsTabs}
defaultSelectedTabId={selectedSalesTab}
/>
<div>{getPageContent}</div>
</div>
);
};
export default SalesReports;
this is the Child component I'm using
const Dashboard: FC = () => {
const repostsRxjs = rxjsConfig(reportingAxios);
const dispatch = useDispatch();
useEffect(() => {
repostsRxjs
.post<SalesDashboardItem[]>(
'/sales-data/order-details/6087bc3606ff073930a10848?timezone=Asia/Dubai&from=2022-09-03T00:00:00.00Z&to=2022-12-25T00:00:00.00Z&size=10',
{
brandIds: [],
channelIds: [],
kitchenIds: [],
countryIds: [],
},
)
.pipe(
take(1),
catchError((err: any) => of(console.log(err))),
)
.subscribe((response: SalesDashboardItem[] | void) => {
if (response) {
dispatch(reportsActionCreators.changeSalesDashboardData(response));
}
});
}, []);
const { isActiveFilter } = useSelector<RootState, any>((state: RootState) => {
return {
isActiveFilter: state.filterData.isActiveFilter,
};
});
return (
<>
<div
onClick={() => {
dispatch(filterssActionCreators.handleFilterPanel(!isActiveFilter));
dispatch(
filterssActionCreators.changeSelectedFiltersType([
FilterTypes.BRAND,
FilterTypes.CHANNEL,
FilterTypes.COUNTRY,
FilterTypes.KITCHEN,
]),
);
}}
>
Dashboard
</div>
{isActiveFilter && <FilterPanel />}
</>
);
};
export default Dashboard;
Actions
import { SalesDashboardItem } from 'app/models/Reports';
import { actionCreator } from 'app/state/common';
export type ChangeSalesTabPayload = string;
export type ChangeSalesDashboardDataPayload = SalesDashboardItem[];
export const reportsActionTypes = {
CHANGE_SALES_TAB: 'CHANGE_SALES_TAB',
CHANGE_SALES_DASHABOARD_DATA: 'CHANGE_SALES_DASHABOARD_DATA',
};
export const reportsActionCreators = {
changeSalesTab: actionCreator<ChangeSalesTabPayload>(reportsActionTypes.CHANGE_SALES_TAB),
changeSalesDashboardData: actionCreator<ChangeSalesDashboardDataPayload>(
reportsActionTypes.CHANGE_SALES_DASHABOARD_DATA,
),
};
export type ReportsAction = {
type: typeof reportsActionTypes.CHANGE_SALES_TAB | typeof reportsActionTypes.CHANGE_SALES_DASHABOARD_DATA;
payload: ChangeSalesTabPayload | ChangeSalesDashboardDataPayload;
};
Reducer
import { SalesDashboardItem } from 'app/models/Reports';
import { salesReportsTabs } from 'app/utils/reports';
import { reportsActionTypes, ReportsAction } from './actions';
export type ReportsState = {
selectedSalesTab: string;
salesDashboardFilterData: {
brands: string[];
kitchens: string[];
channels: string[];
countries: string[];
};
salesDashBoardDatta: SalesDashboardItem[];
};
const initialState: ReportsState = {
selectedSalesTab: salesReportsTabs[0].id,
salesDashboardFilterData: {
brands: [],
kitchens: [],
channels: [],
countries: [],
},
salesDashBoardDatta: [],
};
export default (state = initialState, action: ReportsAction): ReportsState => {
switch (action.type) {
case reportsActionTypes.CHANGE_SALES_TAB:
return { ...state, selectedSalesTab: action.payload as string };
case reportsActionTypes.CHANGE_SALES_DASHABOARD_DATA:
return { ...state, salesDashBoardDatta: action.payload as SalesDashboardItem[] };
default:
return state;
}
};
root reducer
import { combineReducers } from 'redux';
import SidePanelReducer from './reducers/sidepanel.reducer';
import authReducer from './auth';
import onboardingReducer from './onboarding';
import applicationReducer from './application';
import inventoryConfigReducer from './inventoryConfig/inventory.reducer';
import reportsReducer from './reports';
import filtersReducer from './filter';
const rootReducer = combineReducers({
sidePanel: SidePanelReducer,
auth: authReducer,
onboarding: onboardingReducer,
application: applicationReducer,
inventory: inventoryConfigReducer,
reports: reportsReducer,
filterData: filtersReducer,
});
export default rootReducer;
when I'm adding the dispatch action in useEffect(componentDidMount) this looping is happening. Otherwise, this code works fine. How can I avoid that component rerendering?
I think the issue is that the useSelector hook is returning a new object reference each time which triggers the useMemo hook to re-memoize an "instance" of the Dashboard component. The new "instance" of Dashboard then mounts and runs its useEffect hook which dispatches an action that updates the state.reports state in the Redux store.
Instead of creating and returning a new object reference to destructure selectedSalesTab from, just return the state.reports object directly.
Change
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return {
selectedSalesTab: state.reports.selectedSalesTab,
};
});
to
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return state.reports;
});

TypeError: useContext(...) is undefined

I'm trying to use a custom hook that bring me functions to handle my TODOS on my context, but it gives me an error
Uncaught TypeError: useContext(...) is undefined
The above error occurred in the component:
Complete Error Image
TodoProvider.jsx
import { useReducer } from 'react';
import { useTodos } from '../hooks/useTodos';
import { TodoContext, todoReducer } from './';
export const TodoProvider = ({ children }) => {
const init = () => {
return [];
};
const [todos, dispatchTodos] = useReducer(todoReducer, {}, init);
const { handleNewTodo, handleToggleTodo } = useTodos();
return (
<TodoContext.Provider
value={{ todos, dispatchTodos, handleNewTodo, handleToggleTodo }}
>
{children}
</TodoContext.Provider>
);
};
useTodos.js
import { useContext } from 'react';
import { TodoContext } from '../context';
import { types } from '../types/types';
export const useTodos = () => {
const { dispatchTodos } = useContext(TodoContext);
const handleNewTodo = todo => {
const action = {
type: types.add,
payload: todo,
};
dispatchTodos(action);
};
const handleToggleTodo = id => {
dispatchTodos({
type: types.toggle,
payload: id,
});
};
return { handleNewTodo, handleToggleTodo };
};
The error traceback in your image says
`useContext(...)` is not defined
useTodos (useTodos.js:6)
Since you aren't showing your useTodos.js file, I must rely on my crystal ball to tell me that you've forgotten to
import {useContext} from 'react';
in useTodos.js, hence "not defined".
Here's an one-file example based on your code that verifiably does work...
import { useReducer, useContext, createContext } from "react";
function todoReducer(state, action) {
switch (action.type) {
case "add":
return [...state, { id: +new Date(), text: action.payload }];
default:
return state;
}
}
const TodoContext = createContext([]);
const TodoProvider = ({ children }) => {
const [todos, dispatchTodos] = useReducer(todoReducer, null, () => []);
return (
<TodoContext.Provider value={{ todos, dispatchTodos }}>
{children}
</TodoContext.Provider>
);
};
function useTodoActions() {
const { dispatchTodos } = useContext(TodoContext);
function handleNewTodo(todo) {
dispatchTodos({
type: "add",
payload: todo
});
}
function handleToggleTodo(id) {
dispatchTodos({
type: "toggle",
payload: id
});
}
return { handleNewTodo, handleToggleTodo };
}
function useTodos() {
return useContext(TodoContext).todos;
}
function TodoApp() {
const todos = useTodos();
const { handleNewTodo } = useTodoActions();
return (
<div>
{JSON.stringify(todos)}
<hr />
<button onClick={() => handleNewTodo((+new Date()).toString(36))}>
Add todo
</button>
</div>
);
}
export default function App() {
return (
<TodoProvider>
<TodoApp />
</TodoProvider>
);
}

useSelector cause infinate loop in useEffect or passed props is not the newest state in redux

When i add columnext into useEffect dependencies it caused a infnate loop,but when i removed
columnext from denpendencies,the prop materialExtValues passed to my child Component MaterialForm is not the newest redux state but the previous state,my child component render the wrong data.I tried my solution on stackoverflow but can't get my except result,I am really confused,Who can help my out?
import React, { useCallback, useEffect, useState } from 'react';
import FormModal from '../../../../components/FormModal/FormModal';
import { FormType, ColumnExt } from '../../../../types';
import {
MaterialValues,
initialMaterialValues,
} from '../MaterialValues/MaterialValues';
import MaterialForm from './MaterialForm';
import { cvtNullToEmpty } from '../../../../helpers/cvtNullToEmpty';
import { useDispatch, useSelector } from 'react-redux';
import { selectColumnExtDataSelector } from '../../../../redux/columnext/columnext.selector';
import {
materialExtValues,
ExtValues,
EXT_KEYS,
} from '../MaterialValues/MaterialValues';
import { fetchColumnextsRequest } from '../../../../redux/columnext/columnext.action';
interface MaterialEditProps {
editItem: string;
initialValues: MaterialValues;
handleClose: () => void;
}
const MaterialEdit: React.FC<MaterialEditProps> = ({
editItem,
initialValues,
handleClose,
}) => {
const dispatch = useDispatch();
const columnexts: ColumnExt[] = useSelector(selectColumnExtDataSelector);
const [extValues, setExtValues] = useState<ExtValues>(materialExtValues);
//get newest extValues
const initExtValues = useCallback(() => {
const colextFormData = new FormData();
colextFormData.append('TableName', 'material');
colextFormData.append('ObjectId', editItem);
dispatch(fetchColumnextsRequest(colextFormData));
}, [editItem, dispatch]);
//combine newest extValues with old extValues
const mergeMaterialExtValues = useCallback(() => {
const materialExtMerge: ExtValues = {};
columnexts.forEach((item) => {
EXT_KEYS.forEach((key) => {
if (item[key] !== '') {
materialExtMerge[`${item.ColumnName}__${key}`] = item[key];
}
});
});
console.log('materialExtMerge', materialExtMerge);
const newExts = Object.assign(materialExtValues, materialExtMerge);
setExtValues((prev) => ({ ...prev, ...newExts }));
console.log('materialExtValues', materialExtValues);
}, [columnexts]);
useEffect(() => {
initExtValues();
}, [initExtValues, columnexts]);
useEffect(() => {
if (columnexts.length > 0 && columnexts[0].ObjectId === editItem) {
mergeMaterialExtValues();
}
}, [mergeMaterialExtValues, editItem, columnexts.length]);
return (
<>
<div className='material-edit'>
<FormModal
title='Edit Material'
iconSrc='/assets/images/icons/material.png'
handleClose={handleClose}
renderDataForm={() => (
<MaterialForm
formType={FormType.EDIT}
editItem={editItem}
materialExtValues={extValues}
initialValues={
(cvtNullToEmpty(initialValues) as MaterialValues) ||
initialMaterialValues
}
handleClose={handleClose}
/>
)}
/>
</div>
</>
);
};
export default MaterialEdit;
The code of selectColumnExtDataSelector is :
import { RootState } from "../rootReducer";
import { createSelector } from "reselect";
export const selectColumnExts = (state: RootState) =>
state.columnext
export const selectColumnExtDataSelector = createSelector(
[selectColumnExts],
columnexts => columnexts.data
)
And ColumnExtReducer code is:
import { ColumnExt } from "src/types"
import { AnyAction } from 'redux';
import { columnextActionType } from "./columnext.types";
export interface ColumnExtState {
data: ColumnExt[],
loading: boolean;
error: string | null;
}
const initialState: ColumnExtState = {
data: [],
loading: false,
error: null
}
const columnextReducer = (state: ColumnExtState = initialState,
action: AnyAction
) => {
switch (action.type) {
case columnextActionType.FETCH_COLUMNEXTS_REQUEST:
return { ...state, loading: true }
case columnextActionType.FETCH_COLUMNEXTS_SUCCESS:
return { ...state, loading: false, data: action.payload }
case columnextActionType.FETCH_COLUMNEXTS_FAILURE:
return { ...state, loading: true, error: action.payload }
default:
return state;
}
}
export default columnextReducer;

Redux action not firing onclick

Doing a redux todo to learn and having trouble on the toggle todo.
When i trigger the onclick i get an error in the console.
"Failed prop type: The prop items is marked as required in ItemsList, but its value is undefined."
and
"Cannot read property 'length' of undefined" on items.length
I've consoled logged the action and it seems to be returning the state. not sure what i'm doing wrong. code is below.
actions
export const addItem = content => {
return { type: ADD_ITEM, content };
};
export const toggleTodo = (id) => {
return {
type: TOGGLE_TODO,
id,
};
};
my initial state is:
import { ADD_ITEM, TOGGLE_TODO } from './constants';
let nextId = 4;
export const initialState = {
items: [
{ id: 1, content: 'Call mum', completed: false},
{ id: 2, content: 'Buy cat food', completed: true },
{ id: 3, content: 'Water the plants', completed: false },
],
};
const reducer = (state = initialState, action) => {
console.log("action", action.type);
switch (action.type) {
case ADD_ITEM:
const newItem = {
id: nextId++,
content: action.content,
completed: false
};
return {
...state,
items: [...state.items, newItem],
};
case TOGGLE_TODO:
return state.items.map(todo => {
console.log("state", state);
console.log("state.items", state.items);
console.log("todo",todo);
if (todo.id !== action.id) {
return state;
}
return {
...state,
completed: !todo.completed,
};
});
default:
return state;
}
};
export default reducer;
and my list component is
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Todo from "../ItemTodo/index"
import { toggleTodo } from "../../logic/actions";
import './styles.css';
export const ItemsList = ({ items, onTodoClick }) => {
return (
<div>
<ul className={'itemsList-ul'}>
{items.length < 1 && <p id={'items-missing'}>Add some tasks above.</p>}
{items.map(item =>
<Todo
key={item.id}
{...item}
onClick={() => onTodoClick(item.id)}
/>
)}
</ul>
</div>
);
};
ItemsList.propTypes = {
items: PropTypes.array.isRequired,
onTodoClick: PropTypes.func.isRequired,
};
const mapStateToProps = state => {
return { items: state.todos.items };
};
const mapDispatchToProps = dispatch => ({
onTodoClick: id => dispatch(toggleTodo(id))
});
export default connect(mapStateToProps, mapDispatchToProps)(ItemsList);
and my todo component is
import React from 'react';
import PropTypes from 'prop-types';
const Todo = ({onClick, completed, content }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none',
}}
>
{content}
</li>
);
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
content: PropTypes.string.isRequired,
};
export default Todo;
Create store
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import configureStore from './redux/store';
import Header from './components/Header';
import ItemCreator from './components/ItemCreator';
import ItemsList from './components/ItemsList';
import './app.css';
const store = configureStore();
class App extends Component {
render() {
return (
<Provider store={store}>
<div className="app">
<Header />
<div>
<ItemCreator />
<ItemsList />
</div>
</div>
</Provider>
);
}
}
export default App;
import { createStore, applyMiddleware, compose } from 'redux';
import createReducer from './reducers';
const composeEnhancers =
(process.env.NODE_ENV !== 'production' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
const configureStore = (initialState = {}) => {
return createStore(
createReducer(),
initialState,
composeEnhancers(applyMiddleware())
);
};
export default configureStore;
import { combineReducers } from 'redux';
import reducer from '../logic/reducer';
export default function createReducer() {
return combineReducers({
todos: reducer,
});
}
I haven't tested it, but I think this part is incorrect TOGGLE_TODO:
return state.items.map(todo => {
console.log("state", state);
console.log("state.items", state.items);
console.log("todo",todo);
if (todo.id !== action.id) {
return state;
}
return {
...state,
completed: !todo.completed,
};
});
it should be:
return {
...state,
items: state.items.map((todo) => {
if (todo.id === action.id) {
return { ...todo, completed: !todo.completed };
}
return todo;
})
};

React Redux new data replacing current data instead of extend it and function only run once

I'm using this package https://github.com/RealScout/redux-infinite-scroll to make infinite scroll on list of brand. Here is my code:
Container:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions, getBrands } from '../reducer';
import Infinite from 'react-infinite';
import InfiniteScroll from 'redux-infinite-scroll';
import SearchBox from '../components/SearchBox';
import CardList from '../components/CardList';
const { fetchBrands } = actions;
class BrandList extends Component {
componentDidMount() {
this.props.fetchBrands({ page: 1 });
}
renderList() {
const brands = this.props.brands;
return brands.map((brand) => {
return (
<CardList key={brand.id} name={brand.name} avatar={brand.avatar.thumbnail} follower={brand.follows_count} />
);
});
}
toggle() {
return this.props.isFetching;
}
loadMore() {
const {lastPage, currentPage} = this.props;
const nextPage = currentPage ? parseInt(currentPage) + 1 : 1;
if(currentPage && currentPage <= lastPage){
this.props.fetchBrands({page: nextPage});
}
}
render() {
return (
<div>
<SearchBox />
<div className="row">
<InfiniteScroll
items={this.renderList()}
loadMore={this.loadMore.bind(this)}
/>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
brands: getBrands(state),
isFetching: state.brand.isFetching,
currentPage: state.brand.currentPage,
lastPage: state.brand.lastPage
};
}
export default connect(mapStateToProps, { fetchBrands })(BrandList);
Reducer:
import axios from 'axios';
// Define Types
export const types = {
// brand list
FETCH_BRANDS: 'fetch_brands',
FETCH_BRANDS_SUCCESS: 'fetch_brands_success',
FETCH_BRANDS_ERROR: 'fetch_brands_failure',
FETCH_BRAND: 'fetch_brand',
FETCH_BRAND_SUCCESS: 'fetch_brand_success',
FETCH_BRAND_ERROR: 'fetch_brand_failure',
};
const { FETCH_BRANDS, FETCH_BRANDS_SUCCESS, FETCH_BRANDS_ERROR } = types;
// Define Reducer
export const INITIAL_STATE = { brands: [], brand: {}, isFetching: false, error: null, currentPage: 1 };
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_BRANDS:
return { ...state, isFetching: true };
case FETCH_BRANDS_SUCCESS:
return { ...state, brands: action.payload.brands.data, currentPage: action.payload.brands.current_page, lastPage: action.payload.brands.last_page };
case FETCH_BRANDS_ERROR:
return { ...state, error: action.payload };
default:
return state;
}
}
// Define Actions
export const actions = {
fetchBrands: ({page, count = 15}) => {
return (dispatch) => {
dispatch({ type: FETCH_BRANDS });
axios.get(`brands?page=${page}&count=${count}`)
.then((response) => {
const {data} = response;
if (data.code == 200) {
dispatch({ type: FETCH_BRANDS_SUCCESS, payload: data });
}
});
};
}
};
// SELECTOR
export const getBrands = (state) => state.brand.brands;
it run loadMore function successfully but it not extend current list, it replace it instead.
loadmore function only run once. it should run 10 times.
do I miss something on my code to make it scroll?
Try adding
brands: [ ...state.brands, ...action.payload.brands.data]
like this in your reducer
case FETCH_BRANDS_SUCCESS:
return { ...state, brands: [ ...state.brands, ...action.payload.brands.data], currentPage: action.payload.brands.current_page, lastPage: action.payload.brands.last_page };
Which means that you are concating current list with upcoming list (versioned data)

Resources