Redux automatically adds an entry called state. Practice creating counter function. However, the state item that I did not set is automatically added so that it does not appear in the desired direction when dispacth is executed.
I guess it's caused by combineReducers, but I don't know exactly.
Clearly, I didn't make State in counter.
//action name
const INCRE = 'counter/INCRE';
const DECRE = 'counter/DECRE';
export const incre = () => ({type: INCRE});
export const decre = () => ({type: DECRE});
//default
const initState = {
number: 0
};
//reducer
function counter(state = initState, action) {
console.log("frist value");
console.log(state.number);
console.log(initState);
if(action.type === INCRE){
console.log("add");
console.log(state.number)
return {number: state.number +1 }
}else if (action.type === DECRE) {
console.log("minus");
return {number: state.number -1 }
}
else {
return{state};
};
};
export default counter;
// C is containers & Counter is name
import React from 'react';
import Counter from '../components/Counter';
import { connect } from 'react-redux';
import {incre, decre} from '../modules/counter';
const CCounter = ({number, incre, decre}) => {
return (
<Counter number={number} onIncre={incre} onDecre={decre}/>
);
};
const mapStateToProps = state => ({
number: state.counter.number
});
const mapDispatchToProps = dispatch => ({
incre: () => {
dispatch(incre());
},
decre: () => {
dispatch(decre());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(CCounter);
//reducer
//...
else {
return{state};
};
The above line in your reducer is likely the problem because what it can return is what you're seeing:
{
state: {
number: 0,
}
}
You probably wanted to simply return the state.
else {
return state;
};
Related
below is my code to update the state.customer slice. However when this runs, I see that the next state is not updated with the payload dispatched from the action. can some one please point out what I'm missing?
reducer:
const initialState1 = {"customer":{}};
const customer = (state = initialState1, action) => {
console.log('state:', state);
switch (action.type) {
case 'INIT_CUSTOMER_INFO':
return {
...state,
customer: action.payload
}
case 'UPD_CUSTOMER_INFO':
console.log(action);
return { ...customer, firstname: action.firstname }
default: return state;
}
}
export default customer;
action
export const initCustomer = customer => {
return {
type: 'INIT_CUSTOMER_INFO',
payload : customer
}
}
dispatch from LWC
import { LightningElement, track } from 'lwc';
import { connect } from 'c/connect';
import { updateCustomer, initCustomer } from 'c/actions';
import getJSONData from '#salesforce/apex/RGClass.getCartSummary';
const mapStateToProps = (state, ownProps) => ({
customer: state.customer
})
const mapDispatchToProps = (dispatch, ownProps) => ({
initCustomer : customer => dispatch(initCustomer(customer)),
updateCustomer : customer => dispatch(updateCustomer(customer))
})
export default class DigiForm extends LightningElement {
#track firstname;
showfirstname;
connectedCallback() {
//add the hook
connect(mapStateToProps, mapDispatchToProps)(this);//connects the
//api call
getJSONData()
.then(result => {
console.log('result:',result);
this.initCustomer(result);
})
.catch(error => {
console.log(error);
});
}
onContinue = () => {
let fn = this.template.querySelector('lightning-input').value;
console.log('firstname:', fn);
this.updateCustomer({ firstname : fn});
if(fn != null){
this.showfirstname = true;
}
}
}
Tried a couple of options but none worked. any help would be much appreciated!
this my updated version of intergrating redux and NextJS. Just to elobarate what I have done so far...
STEP 1. I've created a store.js file to set up my global store in reference to github's explanation from nextJS developers.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { createWrapper, HYDRATE } from 'next-redux-wrapper';
import thunkMiddleware from 'redux-thunk';
import { customerListReducer } from './customerReducers';
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension');
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
const combinedReducer = combineReducers({
customerList: customerListReducer,
});
const reducer = (state, action) => {
console.log('Just Displaying the Store', state);
if (action.type === HYDRATE) {
const nextState = {
...state, // use previous state
...action.payload, // apply delta from hydration
};
if (state.count) nextState.count = state.count; // preserve count value on client side navigation
return nextState;
} else {
return combinedReducer(state, action);
}
};
// create a makeStore function
const store = () =>
createStore(
reducer,
bindMiddleware([thunkMiddleware])
);
// export an assembled wrapper
export const wrapper = createWrapper(store);
STEP 2: Imported the wrapper above in my _app file to make the wrapper available across all pages in my application
import Nav from '../components/Nav';
import {wrapper} from '../reducers/store';
function MyApp({ Component, pageProps }) {
return (
<>
<Nav />
<Component {...pageProps} />
</>
);
}
export default wrapper.withRedux(MyApp);
STEP 3: CONFIGURATIONS
A) My Action that calls external API
import axios from 'axios';
import {
CUSTOMER_LIST_REQUEST,
CUSTOMER_LIST_SUCCESS,
CUSTOMER_LIST_FAIL,
} from '../constants/customerConstants';
export const listCustomers = () => async (dispatch) => {
try {
dispatch({
type: CUSTOMER_LIST_REQUEST,
});
const { data } = await axios.get(
'https://byronochara.tech/gassystem/api/v1/customers'
);
const result = data.results;
dispatch({
type: CUSTOMER_LIST_SUCCESS,
payload: result,
});
} catch (error) {
dispatch({
type: CUSTOMER_LIST_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
B)My Action Reducer
import {
CUSTOMER_LIST_REQUEST,
CUSTOMER_LIST_SUCCESS,
CUSTOMER_LIST_FAIL,
} from '../constants/customerConstants';
import { HYDRATE } from 'next-redux-wrapper';
export const customerListReducer = (state = { customers: [] }, action) => {
switch (action.type) {
case HYDRATE:
return { loading: true, customers: [] };
case CUSTOMER_LIST_REQUEST:
return { loading: true, customers: [] };
case CUSTOMER_LIST_SUCCESS:
return {
loading: false,
customers: action.payload,
};
case CUSTOMER_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
C)The finally bringing it all together in my index.js page to display the results:
import React, { useEffect } from 'react';
import Head from 'next/head';
import { useSelector} from 'react-redux';
import { listCustomers } from './../actions/customerActions';
import { wrapper } from '../reducers/store';
import styles from '../styles/Home.module.css';
const Home = () => {
//Select the loaded customers' list from central state
const customerList = useSelector((state) => {
console.log(state);
return state.customerList;
});
const { loading, error, customers } = customerList;
//displaying the customers data from the external API
console.log('Fetched Customers Data', customers);
return (
<div className={styles.container}>
<Head>
<title>Home | Next</title>
</Head>
<h1>Welcome to Home Page</h1>
{/* {loading && <h6>Loading...</h6>} */}
{/* {error && <h6>Error Occured...</h6>} */}
{/* {customers.map((customer) => (
<h3>{customer.customerName}</h3>
))} */}
{/* <ArticleList customers={customers} /> */}
</div>
);
};
// getStaticProp at build time
// getServerSideProp at every request slower
// getStaticPath to dynamically generate paths based on the data we are fetching
export const getStaticProps = wrapper.getServerSideProps(async ({ store }) => {
// console.log('STORE', store);
store.dispatch(listCustomers());
});
export default Home;
COMMENT ON THE PROBLEM I'M FACING FROM THE ABOVE CODE: once everything has been set up if you follow the code above, the code seems to run well the store is successfully created when I log the result on the console ``{ customerList: { loading: true, customers: [] } }. But then I guess this is the result from the HYDRATE action type since it will always be dispatch since am using getStaticProps``` that creates a new store instance in the server.
MAIN QUIZ: My challenge is how do I bypass the HYDRATED action and reconcile the server side state with the client side store and persist it and at least to finally be able to view the list from the external API. Thanks in advance. :)
I totally recommend you to use reduxjs/toolkit. It's very simple , less code, no wrappers, clean. And no matter your project on nextjs or created via CRA. Also you dont need to configure redux-thunk and redux-devtools cause they are enabled by default. Read documentation for more information ( how to persist state without any npm package and so on )
Here is a little example.
store.js
import { combineReducers, configureStore } from "#reduxjs/toolkit";
import userSlice from './user.slice.js';
//reducers
const rootReducer = combineReducers({
user: userSlice
});
const store = configureStore({
reducer: rootReducer,
});
export default store;
Wrap with Provider (in your case _app.js)
<Provider store={store}>
<Component {...pageProps} />
</Provider>
user.slice.js ( action + reducer )
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
const initialState = {
id: '',
email: '',
roles: []
};
// export async action
export const signIn = createAsyncThunk('user/signIn', async (data) => {
try {
const payload = await api.auth.signin(data).then((res) => res.data);
// do some stuff if you want
return payload ;
} catch (err) {
console.log(err.response);
}
});
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
removeUser(state, payload) {
//cant be an async method
return initialState;
},
extraReducers: (builder) => {
builder.addCase(signIn.fulfilled, (state, { payload }) => {
// payload from the async method above (asyncThunk)
return payload;
});
},
},
});
// export actions
export const { removeUser } = userSlice.actions;
// export reducer
export default userSlice.reducer;
Thats it. Last step to call actions from any component e.g.
import { useDispatch, useSelector } from 'react-redux';
import { signIn, removeUser } from '../actions/userSlice';
// in function component
// call hooks
const dispatch = useDispatch();
// read the store
const { user } = useSelector((state) => state);
// dispatch any action , example below
dispatch(signIn(userCredentials));
// or
dispatch(removeUser());
I has an Issue with setting Redux with NextJS and this is my final answer after some insight from mirik999 too.
A. my store.
import { configureStore } from '#reduxjs/toolkit';
//importing the slice file with sliced reducers
import customerListReducer from '../slices/customerSlice';
// const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware));
const store = configureStore({
reducer: {
customerList: customerListReducer,
},
});
export default store;
B. The store is provided in my app component
function MyApp({ Component, pageProps }) {
return (
<Provider store={store}>
<Nav />
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
C. The Slice file that automatically creates action creators and the reducer
import { createSlice } from '#reduxjs/toolkit';
//creating and action that calls API from a REST API backend
export const customersFetchedList = createAsyncThunk(
'customersList/customersListSuccess',
async () => {
try {
const { data } = await axios.get(
'https://example.com/api/your/endpoint'
);
const result = data.results;
//the payload
const payload = result;
return payload;
} catch (error) {
console.log(error.response);
const payload =
error.response && error.response.data.message
? error.response.data.message
: error.message;
return payload;
}
}
);
const initialState = {
loading: true,
customers: [],
error: false,
};
const customerListSlice = createSlice({
name: 'customersList',
initialState,
reducers: {
//reducer functions we've provided
customersRequest(state, action) {
if (state.loading == true) {
return state;
}
},
},
extraReducers: (builder) => {
initialState,
builder.addCase(customersFetchedList.fulfilled, (state, action) => {
state.loading = false;
state.customers = action.payload;
state.error = false;
return state;
});
},
});
export const {
customersRequest,
customersLoadingError,
} = customerListSlice.actions;
export default customerListSlice.reducer;
D. Then finally fired this action above in my component using the useEffect()
import React, { useEffect } from 'react';
import Head from 'next/head';
const Home = () => {
//method to fire the action
const dispatch = useDispatch();
//Select the loaded customers' list from central state
const customerList = useSelector((state) => state);
// const { loading, error, customers } = customerList;
useEffect(() => {
dispatch(listCustomers());
}, []);
return (
<div className={styles.container}>
<Head>
<title>Home | Next</title>
</Head>
<h1>Welcome to Home Page</h1>
{loading && <h6>Loading...</h6>}
{error && <h6>Error Occured...</h6>}
{customers.map((customer) => (
<h3>{customer.customerName}</h3>
))}
</div>
);
};
Thanks so much for your contribution. :)
I learn React-Redux and need help understanding why this Component only works on start but not when I press the button.
When debug start the breakpoints in the picture break execution but when I press the button I get this error showed in the picture.
When breakpoints hit I hoower over the {toasts.map(toast => { and the Array size is zero. But when I press button the breakpoints does not even hit
Any ide?
UPDATE
I have this configureStore.js
import { combineReducers } from "redux";
import { createStore, applyMiddleware, compose } from "redux";
import { forbiddenWordsMiddleware } from "../middleware";
import ToastsReducer from '../reducers/ToastsReducer';
import RootReducer from '../reducers/RootReducer';
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const reducers = {
toastsReducer: ToastsReducer,
rootReducer: RootReducer
};
const reduce = combineReducers({
...reducers,
});
const store = createStore(
reduce,
storeEnhancers(applyMiddleware(forbiddenWordsMiddleware))
);
export default store;
RootReducer.js
import { ADD_ARTICLE } from "../constants/action-types";
import { FOUND_BAD_WORD } from "../constants/action-types";
const initialState = {
articles: []
};
export default function reducer(state = initialState, action) {
if (action.type === ADD_ARTICLE) {
return Object.assign({}, state, {
articles: state.articles.concat(action.payload)
});
}
if (action.type === FOUND_BAD_WORD) {
//return Object.assign({}, state, {
// articles: state.articles.concat(action.payload)
// });
}
return state;
}
ToastsReducer.js
import { ADD_TOAST, REMOVE_TOAST } from "../constants/action-types";
const initialState = {
toastList: []
};
export default function toasts(state = initialState, action) {
const { payload, type } = action;
switch (type) {
case ADD_TOAST:
return [payload, state.toastList];
case REMOVE_TOAST:
return state.toastList.filter(toast => toast.id !== payload);
default:
return state;
}
}
UPDATE
Picture showing RootReducer.jsx and Toasts.jsx when I press button two times,
Toast.js
import PropTypes from "prop-types";
import React, { Component } from "react";
class Toast extends Component {
render() {
return (
<li className="toast" style={{ backgroundColor: this.props.color }}>
<p className="toast__content">
{this.props.text}
</p>
<button className="toast__dismiss" onClick={this.props.onDismissClick}>
x
</button>
</li>
);
}
shouldComponentUpdate() {
return false;
}
}
Toast.propTypes = {
color: PropTypes.string.isRequired,
onDismissClick: PropTypes.func.isRequired,
text: PropTypes.string.isRequired
};
export default Toast;
Please share your reducer code. Most likely, you have not set an initial state for toastList in the reducer or there is an error with toastsReducer.toastList.
Try the following:
Change line 34 to toasts: state.toastsReducer
Comment the lines from 10 to 19 and insert the following to make sure toasts is an array.
console.log(toasts);
console.log(toasts.toastList);
return null;
If both are undefined, then the value returned by the reducer is not right.
In ToastsReducer.js:
Change the following:
case ADD_TOAST:
return [ ...state.toastList, payload]; //<--- Here
When you do return[payload,state.toastList], it appends another array to the toastList.
Run the following to see:
toastList = ['abc'];
// Right way to add an item to an array.
toastList = [...toastList, 'def'];
console.log(toastList);
console.log('-----');
// Adds an array to the array. Incorrect way.
toastList = [toastList, 'ghi'];
console.log(toastList);
---UPDATE---
Change your ADD_TOAST case to:
return { toastList: [...state.toastList, payload] };
and you should be good to go.
Just do check your toasts array contains data,
{toasts && toasts.length > 0 ? toasts.map(toast => {...}) : null}
That's the component in question. Before the component is mounted, it successfully dispatches an action {this.props.populateGrid()}. Everything is fine, I can see the state in the logger (basically it's a nested array of random numbers). When I press the button, it should rehydrate the state with new random numbers. Yet, I get the following error: Cannot read property 'populateGrid' of undefined.
import React, { Component, PropTypes } from 'react';
import { View, StyleSheet, Button } from 'react-native';
import Grid from './Grid';
import * as globalStyles from '../styles/global';
export default class Body extends Component {
componentWillMount() {
this.refresh();
}
refresh() {
this.props.populateGrid();
}
render() {
return (
<View style={styles.body}>
<Grid inGrid={this.props.grid} />
<Button
onPress={this.refresh}
title={'Regenerate the Grid'}
/>
</View>
);
}
}
Container:
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { listNumbers, pickNumber } from '../actions/numberActions';
import { populateRow, populateGrid } from '../actions/gridActions';
import Body from '../components/Body';
const mapStateToProps = state => ({
numbers: state.numbers,
grid: state.grid
});
const mapDispatchToProps = dispatch => (
bindActionCreators({
listNumbers,
pickNumber,
populateRow,
populateGrid
}, dispatch)
);
export default connect(
mapStateToProps,
mapDispatchToProps
)(Body);
Action:
import { POPULATE_ROW, POPULATE_GRID } from './actionTypes';
import { randNumbers, randGrid } from '../utils/generators';
export const populateRow = (n) => {
return {
type: POPULATE_ROW,
payload: randNumbers(n)
};
};
export const populateGrid = () => {
return {
type: POPULATE_GRID,
payload: randGrid()
};
};
reducer:
import { POPULATE_ROW, POPULATE_GRID } from '../actions/actionTypes';
export default (state = [], action = {}) => {
switch (action.type) {
case POPULATE_ROW:
return action.payload || [];
case POPULATE_GRID:
return action.payload || [];
default:
return state;
}
};
Generators of numbers (it's the second function in this case)
export const randNumbers = (n) => {
let numbers = new Array(n);
const shuffled = [];
// fill one array with the numbers 1-10
numbers = numbers.fill(1).map((_, i) => i + 1);
// shuffle by taking a random element from one array
// and pushing it to the other array
while (numbers.length) {
const idx = numbers.length * Math.random() | 0; // floor trick
shuffled.push(numbers[idx]);
numbers.splice(idx, 1);
}
return shuffled;
};
export const randGrid = () => {
const shuffled = randNumbers(6);
const array = shuffled.map(a => {
let r = new Array(6);
r = [a, ...randNumbers(5)];
return r;
});
return array;
};
I think you need to bind this to your refresh method in your onClick handler, so that this is set properly when refresh executes:
<Button
onPress={this.refresh.bind(this)}
title={'Regenerate the Grid'}
/>
Hope that helps!
This is the smart component:
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Filter from '../Filter';
import Sort from '../Sort';
import { getItems, selectItem, reverseItems, findItems } from '../../actions/items';
import './app.css';
const App = ({filterList, sortList, onGetItems, onFindItems, reverseItems, onSelectItem}) => {
onGetItems();
return (
<div>
<Filter items={filterList} findText={onFindItems} reverseItems={reverseItems} selectItem={onSelectItem} />
<Sort items={sortList} selectItem={onSelectItem} />
</div>
)}
function mapStateToProps(state) {
return {
filterList: state.items.filter(item => item.name.includes(state.filter.toLowerCase())),
sortList: state.items,
}
}
function matchDispatchToProps(dispatch) {
return bindActionCreators({
onGetItems: getItems,
onSelectItem: selectItem,
onFindItems: findItems,
reverseItems: reverseItems
}, dispatch)}
export default connect(mapStateToProps, matchDispatchToProps)(App);
and actions:
let items = [];
(function onGetItems() {
let xhr = new XMLHttpRequest();
xhr.open('GET', '/items.json', false);
xhr.send();
if (xhr.status !== 200) {
console.log(xhr.status + ': ' + xhr.statusText);
} else {
items = JSON.parse(xhr.responseText.toLowerCase());
items.sort(function(a, b) {
if (a.name > b.name) return 1;
if (a.name < b.name) return -1;
return 0;
});
}
})();
export const getItems = () => dispatch => {
dispatch({ type: 'ADD_ITEMS', payload: items });
}
export const selectItem = (item) => {
console.log(item);
return {
type: "ITEM_SELECTED",
payload: item
}
};
export const reverseItems = (items) => {
console.log(items)
return {
type: "REVERSE_ITEMS",
payload: items.reverse()
}
};
export const findItems = (items) => {
return {
type: "FIND_ITEMS",
payload: items
}
};
and 2 reducers:
const initialState = '';
export default function filter(state = initialState, action) {
switch (action.type) {
case 'FIND_ITEMS': return action.payload;
default: return state
}
}
const initialState = [];
export default function items(state = initialState, action) {
switch (action.type) {
case 'ADD_ITEMS': return action.payload;
default: return state
}
}
The action reverseItems reverses the array, but the problem is that it doesn't rewrite state because it's formed by another action.
I realize that it's a basic issue, but I can't get how to do that.
Replace your
items.sort() statement with [...items].sort() which creates a new reference to the array and allows the re-rendering of the component. The sort function sorts the array using the same reference and does not cause a re-render.
Try to use Redux Thunk for your async calls.
You can dispatch an action, for example RECEIVED_ITEMS after your http request.