Solution(updated):
I thought any action would cause react-redux-connect to call the mapState functions but when an action doesn't change anything then this is not the case.
I have a localStorage module that dispatches actions but don't change state, instead thy will write to localStorage. The module has selectors that are used in the containers but they won't get called until the state actually changes so the UI would only show correctly after another action was dispatched that would change the state.
Problem
When I put the store on window (window.store=store), add a console.log in the mapStateToProps, then in the console I dispatch an action: store.dispatch({type:'some action'}) then the console.log of the mapStateToProps does not show.
I do memoize the result but the mapStateToProps should be called see here
Full code is here and running example here (you can open a console clicking on 'console' link in the right bottom of the screen).
package.json
store.js:
import { createStore } from 'redux';
export default (initialState, reducer) => {
const store = createStore(
reducer,
initialState,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
window.store = store;
return store;
};
app.js
import React from 'react';
import { connect } from 'react-redux';
import './App.css';
import createStore from './store';
import { Provider } from 'react-redux';
import initCounter from './components/Counter';
import {
createWrapper,
memoize,
} from './components/#common';
const COUNTER = 'COUNTER';
const selectCounterState = state => state.myCounter;
const counter = initCounter({
actionWrapper: createWrapper(COUNTER, 'counter1'),
selectors: { myState: selectCounterState },
connect,
memoize,
});
const initialState = {
myCounter: counter.initialState,
};
const reducer = (state = initialState, action) => {
if (action.emittedBy === COUNTER) {
return {
...state,
myCounter: counter.reducer(
selectCounterState(state),
action.payload
),
};
}
return state;
};
const store = createStore(initialState, reducer);
const Counter = counter.container;
const App = () => (
<Provider store={store}>
<Counter id="counter1" parentId={[]} />
</Provider>
);
export default App;
component/Counter/index:
import component from './component';
const INCREASE = 'INCREASE';
const reducer = (state, action) => {
if (action.type === INCREASE) {
return { ...state, count: state.count + 1 };
}
return state;
};
const makeState = memoize =>
memoize((id, parentId, { count }) => ({
id: parentId.concat(id),
parentId,
count,
}));
const mapStateToProps = ({ myState }, memoize) => () => {
const newState = makeState(memoize);
return (state, ownProps) =>
console.log('in map state to props', new Date()) ||
newState(
ownProps.id,
ownProps.parentId,
myState(state)
);
};
export default ({
actionWrapper,
selectors,
connect,
memoize,
}) => {
const actions = {
increase: ({ id }) =>
actionWrapper({
type: INCREASE,
id,
}),
};
const container = connect(
mapStateToProps(selectors, memoize),
actions
)(component);
return {
container,
reducer,
initialState: { count: 0 },
};
};
components/counter/component.js:
import React from 'react';
export default props => (
<div>
<button onClick={() => props.increase(props)}>
add
</button>
{props.count}
</div>
);
This problem was caused because I had a localStorage module that did dispatch actions but did not change the state, instead it would write to localStorage.
The module had selectors that would get the right data and the containers would use them to build the correct state but since the dispatched action did not change the state in the redux store react-redux would skip calling my mapState functions (probably memoizing state in Provider).
The solution is to let the root reducer return a new state reference {...state} so any action would cause the mapState functions to be called.
Your example codepen works just fine, you just have to trigger an action that gets past your top level guard and is of the expected structure, as to not cause any followup errors:
Post this into the console of your codepen:
store.dispatch({emittedBy: "COUNTER", type: "COUNTER -> INCREASE", id: "counter1", payload: {type: "INCREASE", id: ["counter1"]}})
Related
I am starting a new next js project,we are migrating from a normal react app to a next app.
we intend to use redux toolkit for our global state management and we are using server side rendering.so we came across next-redux-wrapper npm package which looked like it solves most of our problems with redux and ssr but form some reason when ever we use server side rendering on one of the pages the HYDRATE action from next-redux-wrapper is getting called atleast twice sometimes even 4 times.What exactly is going wrong because the article i referred to seems to work fine,i have attached my Redux store details and the Redux slice details,and my getServerSideProps function details.
import { createStore, applyMiddleware, combineReducers } from "redux";
import count from "../ReduxSlices/CounterSlice";
import { createWrapper, HYDRATE } from "next-redux-wrapper";
import { configureStore } from "#reduxjs/toolkit";
const combinedReducer = combineReducers({
count,
});
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const nextState = {
...state, // use previous state
...action.payload, // apply delta from hydration
count: {
count: state.count.count + action.payload.count.count,
},
};
return nextState;
} else {
return combinedReducer(state, action);
}
};
export const makeStore = () =>
configureStore({
reducer: reducer,
});
export const wrapper = createWrapper(makeStore, { debug: true });
and my Redux slice is
import { createSlice } from "#reduxjs/toolkit";
import { HYDRATE } from "next-redux-wrapper";
const initialState = {
count: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState: initialState,
reducers: {
increment: (state) => {
state.count = state.count + 1;
},
},
});
export const { increment } = counterSlice.actions;
export default counterSlice.reducer;
and finally this is how i dispatch an action inside getServerSideProps
export const getServerSideProps = wrapper.getServerSideProps(
(store) => async () => {
store.dispatch(increment());
console.log("server", new Date());
}
);
The console log is logging only once but the HYDRATE action is getting dispatched atleast two times...any insight will be helpful,thank you.
I had the same issue. Disabling react strict mode worked for me.
const nextConfig = {
// reactStrictMode: true,
...
}
module.exports = nextConfig
Would like to seek guidance from folks if this React implementation makes sense and understand the pitfalls if any. The implementation works but I am unsure if its the correct practice. Please kindly advise.
Idea - Create an AppContext that allows reusability of global states (or even functions) - instead of the conventional useContext + useReducer
AppContext.jsx
import React from 'react';
export const AppContext = React.createContext(null);
export const AppContextProvider = (props) => {
const [ appState, setAppState ] = React.useState({});
const appStateProvider = React.useMemo(() => ({ appState, setAppState }), [ appState, setAppState ]);
const setAppStateItem = (key, value) => {
appStateProvider.setAppState(state => { return { ...state, [key]: value} })
return value;
}
const getAppStateItem = (key = '', fallback) => {
return appState[key] || fallback;
}
const deleteAppStateItem = (key = '') => {
if(key in appState) {
appStateProvider.setAppState(state => {
state[key] = undefined;
return state;
})
}
}
return (
<AppContext.Provider value={{ appStateProvider, setAppStateItem, getAppStateItem, deleteAppStateItem }}>
{props.children}
</AppContext.Provider>
)
}
Create.jsx
import React from 'react';
import { AppContext } from 'contexts';
const { setAppStateItem } = React.useContext(AppContext);
....
setAppStateItem('count', 5);
....
Consume.jsx
import React from 'react';
import { AppContext } from 'contexts';
const { getAppStateItem, setAppStateItem } = React.useContext(AppContext);
....
const count = getAppStateItem('count');
....
Here was an approach to create a global state using useContext and useReducer following a pattern similar to redux. You essentially set up a Store with useReducer and a Context.Provider that you then wrap the rest of your application in. Here was a small implementation I had going:
import React, { createContext, useReducer } from "react";
import Reducer from './Reducer'
const initialState = {
openClose: false,
openOpen: false,
ticker: "BTCUSDT",
tickerRows: [],
positionRows: [],
prices: {},
walletRows: []
};
const Store = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>
{children}
</Context.Provider>
)
};
export const ACTIONS = {
SET_CLOSE_OPEN: 'SET_CLOSE_OPEN',
SET_OPEN_OPEN: 'SET_OPEN_OPEN',
SET_TICKER: 'SET_TICKER',
SET_TICKER_ROWS: 'SET_TICKER_ROWS',
SET_POSITION_ROWS: 'SET_POSITION_ROWS',
SET_PRICES: 'SET_PRICES',
SET_WALLET_ROWS: 'SET_WALLET_ROWS'
}
export const Context = createContext(initialState);
export default Store;
Here is the reducer:
import { ACTIONS } from './Store'
const Reducer = (state, action) => {
switch (action.type) {
case ACTIONS.SET_CLOSE_OPEN:
return {
...state,
openClose: action.payload
};
case ACTIONS.SET_OPEN_OPEN:
return {
...state,
openOpen: action.payload
};
...
default:
return state;
}
};
export default Reducer;
I put the Store component in index.js so that it's context is available to all of the components in the app.
ReactDOM.render(
<React.StrictMode>
<Store>
<App />
</Store>
</React.StrictMode>,
document.getElementById('root')
);
Then when you want to access and update the state, you just import the actions and context and the useContext hook:
import { useContext} from "react";
import { ACTIONS, Context } from './store/Store';
and then you just add
const [state, dispatch] = useContext(Context);
inside your functional component and you can access the state and use dispatch to update it.
One limitation is that every component that accesses the state with useContext re-renders every time anything in the state gets updated, not just the part of the state that the component depends on. One way around this is to use the useMemo hook to control when the component re-renders.
export default function WalletTable({ priceDecimals }) {
const classes = useStyles();
const [state, dispatch] = useContext(Context);
async function getCurrentRows() {
const response = await fetch("http://localhost:8000/wallet");
const data = await response.json();
dispatch({ type: ACTIONS.SET_WALLET_ROWS, payload: data });
}
useEffect(() => {
getCurrentRows();
}, []);
const MemoizedWalletTable = useMemo(() => {
return (
<TableContainer component={Paper}>
...
</TableContainer>
);
}, [state.walletRows])
return MemoizedWalletTable
}
Having to memoize everything makes it seem like maybe just using redux isn't all that much more complicated and is easier to deal with once set up.
Newbie to Redux here, I have tried to follow a couple tutorials and I am not clear of how Redux actually works. It was mentioned that the store of Redux is to store the state of the whole tree. I have created and used actions, reducers, and store for my program and it works.
The question is, how do I retrieve what is in the store? Lets say after updating my component, how can I retrieve the value inside the component and to post it?
How can I know what changed in my dropdown list and to retrieve it?
Full code in Sandbox here https://codesandbox.io/s/elated-goldberg-1pogb
store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './RootReducer';
export default function configureStore() {
return createStore(
rootReducer,
applyMiddleware(thunk)
);
}
ProductsList.js
import React from "react";
import { connect } from "react-redux";
import { fetchProducts } from "./SimpleActions";
class ProductList extends React.Component {
constructor(props)
{
super(props);
this.state = {
selecteditems: '',
unitPrice: 0
}
}
componentDidMount() {
this.props.dispatch(fetchProducts());
}
componentDidUpdate(prevProps, prevState) {
if(prevState.selecteditems !== this.state.selecteditems)
{
this.setState((state, props) => ({
unitPrice: ((state.selecteditems * 1).toFixed(2))
}));
}
}
render() {
const { error, loading, products } = this.props;
if (error) {
return <div>Error! {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<select
name="sel"
className="sel"
value={this.state.selecteditems}
onChange={(e) =>
this.setState({selecteditems: e.target.value})}
>
{products.map(item =>
<option key={item.productID} value={item.unitPrice}>
{item.itemName}
</option>
)}
</select>
<p>Unit Price: RM {this.state.unitPrice} </p>
</div>
);
}
}
const mapStateToProps = state => {
const products = state.productsReducer.items;
const loading = state.productsReducer.loading;
const error = state.productsReducer.error;
return {
products,
loading,
error,
}
};
export default connect(mapStateToProps)(ProductList);
SimpleAction.js
export function fetchProducts() {
return dispatch => {
dispatch(fetchProductsBegin());
return fetch('http://localhost:55959/api/products')
.then(handleErrors)
.then(res => res.json())
.then(results => {
dispatch(fetchProductsSuccess(results));
return results;
})
.catch(error => dispatch(fetchProductsFailure(error)));
};
}
function handleErrors(response) {
if(!response.ok) {
throw Error (response.statusText);
}
return response;
}
export const FETCHPRODUCTS_BEGIN = 'FETCHPRODUCTS_BEGIN';
export const FETCHPRODUCTS_SUCCESS = 'FETCHPRODUCTS_SUCCESS';
export const FETCHPRODUCTS_FAILURE = 'FETCHPRODCUTS_FAILURE';
export const fetchProductsBegin = () => ({
type: FETCHPRODUCTS_BEGIN
});
export const fetchProductsSuccess = products => ({
type: FETCHPRODUCTS_SUCCESS,
payload: {products}
});
export const fetchProductsFailure = error => ({
type: FETCHPRODUCTS_FAILURE,
payload: {error}
});
Thanks in advance!
You will need to pass your action handlers to connect function
connect(mapStateToProps,{actions})(ProductList).
how do I retrieve what is in the store? Lets say after updating my component, how can I retrieve the value inside the component and to post it?
if you want to see how is store change, you can add redux-logger to middleware to see that. when store change, it's likely a props change, you can handle this in function componentDidUpdate.
How can I know what changed in my dropdown list and to retrieve it?
values in dropdown is controlled by "const products = state.productsReducer.items;", productsReducer is controlled by actions you passed in dispatch like this: "this.props.dispatch(fetchProducts());".
I think you should add redux-logger to know more how to redux work, it show on console step by step. It will help you learn faster than you think :D
to retrieve it you forgot the selecteditems
const mapStateToProps = state => {
const products = state.productsReducer.items;
const loading = state.productsReducer.loading;
const error = state.productsReducer.error;
const selecteditems = state.prodcuts.selecteditems;
return {
products,
loading,
error,
selecteditems
};
};
To change it you should connect another function like
const mapDispatchToProps = dispatch => {
return {
onChangeDropdownSelection: (selected)=> dispatch(actions.setSelectedDropdown(selected))
}
}
I was learning React and Redux and while doing that I decided to make webpage with a button which on clicking would change the state. Below the button I wanted to display the current state in a different component. Though the button on clicking changes the state, but it is not getting reflected in the component. Here is my code:
App.js
import React from 'react'
import Name from './Name'
import {changeName} from './Action';
export default function App () {
return (
<div>
<button onClick={changeName}>Click me</button>
<Name />
</div>
)
}
Name.js
import React from 'react'
import {store} from './Store'
function Name(props) {
return (
<div>
My name is: {store.getState()}
</div>
)
}
export default Name
Store.js
import { createStore } from 'redux';
import {reducer} from './Reducer';
export const store = createStore(reducer, 'Tarun');
Action.js
import {store} from './Store';
export const changeName = () => {
if (store.getState() === "Tarun"){
store.dispatch({ type: 'name', payload: 'Subhash' });
}
else{
store.dispatch({ type: 'name', payload: 'Tarun' });
}
}
Reducer.js
export const reducer = function(state, action) {
if (action.type === 'name') {
return action.payload;
}
return state;
};
When I click the button, The text inside the Name component does not change. What is the issue?
You need to set up your reducer and initial store properly following the Redux documentation.
You're missing a Provider, which will provide your store to your application.
const store = createStore(reducer, applyMiddleware(thunk));
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
Now, your store is available to your components.
Your reducer needs an initial state too and you're always supposed to return an updated copy of your state. That said, don't change the state directly, but make a copy, change it, then return that copy.
const initialState = {
name: ""
};
const reducer = function(state = initialState, action) {
if (action.type === "name") {
return { ...state, name: action.payload };
} else {
return state;
}
};
export default reducer;
You might have noticed that I added a middleware to your store, and that's because it's usually the way to go when accessing your current reducer's state in your actions. That said, I installed redux-thunk for that, so in your action, you can have something like this:
export const changeName = () => {
return (dispatch, getState) => {
if (getState().name === "Tarun") {
dispatch({ type: "name", payload: "Subhash" });
} else {
dispatch({ type: "name", payload: "Tarun" });
}
};
};
Now, with your store being provided to your app, your reducer being done and your actions being ready to go, you can connect different components to your reducer.
You use the high order component in react-redux called connect for that. For example, in your Name component, we can connect the name to be displayed to your reducer by mapping your state to the component's props:
function Name(props) {
return <div>My name is: {props.name}</div>;
}
const mapStateToProps = state => {
return {
name: state.name
};
};
export default connect(mapStateToProps)(Name);
The nice thing here is that you can also leave the first parameter in the connect high order component empty and just pass the second, which would be the dispatch functions. Well, that's what you would do in your App component, you would connect it to the changeName action.
function App(props) {
return (
<div>
<button onClick={props.changeName}>Click me</button>
<Name />
</div>
);
}
const mapDispatchToProps = dispatch => {
return {
changeName: () => dispatch(changeName())
};
};
export default connect(
null,
mapDispatchToProps
)(App);
Now, when App dispatches a changeName action, your reducer state will be updated and the other components that are connected to the reducer's state will re-render.
Summary: Try to think of your store as an empty jar of candies. Your jar starts empty, but different actions could change what's inside the jar. On top of that, different people in the house that know where the jar is can go get some candy. Translating to your problem, your app begins with an empty name and you have an action that sets up a name. The components that know where to find that name by being connected to your reducer will know when that name changes and will get the updated name.
The final code can be found here:
The only way your name component will rerender is its props or state change, or if a parent component rerenders. Making a change in redux will not automatically do this. In order to see changes to the state, you'd need to subscribe to those changes. You could do this yourself, but a far better solution is to use react-redux, which is designed for connecting react components to redux stores.
For example, you'd add a provider to your app:
import { Provider } from 'react-redux';
import { store } from './Store'
export default function App () {
return (
<Provider store={store}>
<div>
<button onClick={changeName}>Click me</button>
<Name />
</div>
</Provider>
)
}
And then you'd use connect with your Name component:
import { connect } from 'react-redux';
function Name(props) {
return (
<div>
My name is: {props.name}
</div>
)
}
const mapStateToProps = (state) => {
return { name: state };
}
export default connect(mapStateToProps)(Name)
I am trying to set up Redux in React for the first time and I can't seem to pass my initial state from the store to the component. My store file is setting state to the return value of the reducer. Here is what happens when I log this.props to the console
Component
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { exampleAction } from '../../actions';
class Header extends React.Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
console.log(this.props)
return (
<div>
<p>this is {this.props.examplePropOne}</p>
</div>
);
}
}
const mapStateToProps = state => ({
examplePropOne: state.examplePropOne,
examplePropTwo: state.examplePropTwo
});
const mapDispatchToProps = dispatch => {
return bindActionCreators({ exampleAction }, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
Reducer
import { EXAMPLE_ACTION } from './../actions/types'
const initialState = {
examplePropOne : 'Example Property One',
examplePropTwo : 'Example Property Two'
}
export default function (state = initialState, action) {
switch(action.type) {
case EXAMPLE_ACTION:
return {
...state,
examplePropOne: action.payload
}
default:
return state
}
}
Action
import { EXAMPLE_ACTION } from './types'
export const exampleAction = text => ({
type: EXAMPLE_ACTION,
payload: text,
})
[Edit]
Here is what happens when I log the state within mapStateToProps
import React from 'react';
import { createStore, combineReducers } from 'redux';
import reducers from '../reducers';
export const store = createStore(
combineReducers({
state: reducers
}),
);
With how combineReducers() was used with state passed in as a key, your mapStateToProps() would need to look like this instead to access examplePropOne and examplePropTwo:
const mapStateToProps = state => ({
examplePropOne: state.state.examplePropOne,
examplePropTwo: state.state.examplePropTwo
});
Given that combineReducers():
The state produced by combineReducers() namespaces the states of each
reducer under their keys as passed to combineReducers()
The issue is that:
export const store = createStore(
combineReducers({
state: reducers
}),
);
The key state passed to combineReducers() created a namespace/property of state. With the argument named state for the mapStateToProps(), requires that properties are accessed as state.state. This can probably be resolved by instead giving the key passed to combineReducers() a more descriptive name representing what is being used to manage in the store. For example, if it's related to authentication, it could be called some like auth. It would look like:
export const store = createStore(
combineReducers({
auth: reducers
}),
);
// ...
const mapStateToProps = state => ({
examplePropOne: state.auth.examplePropOne,
examplePropTwo: state.auth.examplePropTwo
});
Hopefully that helps!