I have difficulties understanding how the global store should be dispatched with actions from my react components. I'm very new to the whole concept and I don't get my component to re-render on dispatch(). I invested deeply and found that although the reducer returns the updated global state the values are not mapped back to the component props. But a proper function (mapStateToProps) is defined.
Minimal example: Please have a look at this plunkr (or minimal example code below).
Explanation:
I have a component Controls with a method switchActivities. The component is connected to the global store and my global state is available in the component props.
var PullFromStoreControls = function (state) {
return {
concrete: state.me.bool,
nested: state.me.nested.later
}
}
var PushToStoreControls = function (dispatch) {
return {
switchFilter: function (type, value) {
dispatch({
type: 'SET_VAL',
value: value
})
}
}
}
Controls = connect(
PullFromStoreControls,
PushToStoreControls
)(Controls)
I wired the variable state.me.bool to props.conrete to avoid side-effects of a deep state tree. I also connected a dispatcher to update the global state via the reducer. However, if the dispatcher is invoked by 'switchActivities' the new value of the checkbox makes it up to the reducer correctly and then gets lost. The global state seems never updated correctly.
What am I missing?
index.html
<!DOCTYPE html>
<html>
<head>
<script data-require="react#*" data-semver="15.5.0" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.0/react.min.js"></script>
<script data-require="react#*" data-semver="15.5.0" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.0/react-dom.min.js"></script>
<script data-require="redux#*" data-semver="3.2.1" src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.2.1/redux.js"></script>
<script data-require="react-redux#*" data-semver="4.4.5" src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.5/react-redux.js"></script>
<!-- support for jsx on my localhost, on Plunkr jsx will be automatically transpiled to js -->
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<script type = "text/babel" src="minimal.jsx"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
minimal.jsx
function d(x){
console.log(x);
}
const AppState = {
me: {
bool: false,
nested: {
later: "I also want to change values deeper in the tree."
}
}
}
function reducer(state, action) {
if (state === undefined) {
return AppState;
}
switch (action.type) {
case 'SET_VAL':
state.me.bool = action.value;
break;
}
console.log("The reducer returns the changed state");
console.log(state);
return state;
}
// create global store with initial configuration `AppState`
const store = Redux.createStore(reducer, AppState);
// create provider and connect function not having webpack available
var Provider = ReactRedux.Provider;
var connect = ReactRedux.connect;
class Controls extends React.Component {
switchActivities() {
console.log("------------------ clicked ------------------");
console.log("set value from:");
console.log(this.props.concrete);
// inverse current state
const state = !this.props.concrete;
// output
console.log("to:");
console.log(state);
// call dispatcher
this.props.switchFilter("show_act", state);
}
render() {
console.log("I would like to re-render if this.props.concrete has updated!");
const switchActivities = <MapSwitch name="switch_act" label="Show something" checked={this.props.concrete} onChange = {() => this.switchActivities()} />;
return <div id="map-controls">
{switchActivities}
</div>
}
}
var PullFromStoreControls = function (state) {
return {
concrete: state.me.bool,
nested: state.me.nested.later
}
}
var PushToStoreControls = function (dispatch) {
return {
switchFilter: function (type, value) {
dispatch({
type: 'SET_VAL',
value: value
})
}
}
}
Controls = connect(PullFromStoreControls, PushToStoreControls)(Controls)
const MapSwitch = ({name, label, checked, onChange}) => (
<label for={name}>{label}
<input type="checkbox" className="switch" data-toggle="switch"
name={name}
onChange={onChange}
checked={checked}
/>
</label>
)
ReactDOM.render(
<Provider store={store}>
<Controls/>
</Provider>,
document.getElementById('app')
);
Solution (update)
It is a difference whether I alter the state object within the reducer and return that or if I create a new object and return that. Although both returned objects are the same the former is a reference while the latter is a real new variable. I learned that the hard way.
Good explanation:
https://github.com/reactjs/redux/blob/master/docs/recipes/reducers/ImmutableUpdatePatterns.md
function reducer(state, action) {
switch (action.type) {
case 'SET_VAL':
return {
...state,
me : {
...state.me,
bool: action.value
}
}
}
return state;
}
Your problem is that you are mutating state. The second principle of Redux is that the state should never be mutated directly - rather, your reducer is a pure function which should return a new state: https://redux.js.org/docs/introduction/ThreePrinciples.html#changes-are-made-with-pure-functions
Your issue is here:
switch (action.type) {
case 'SET_VAL':
// you are attempting to mutate state.me.bool - this is an antipattern!
state.me.bool = action.value;
break;
}
Instead, write your reducer in a way that returns a new copy of state.
function reducer(state, action) {
switch (action.type) {
case 'SET_VAL':
return {
...state,
me : {
...state.me,
bool: action.value
}
};
default:
return state;
}
}
Notice that you need to copy each level of state for nested structures. I'm using the Object spread operator here, but Object.assign() with all work. Hope this helps!
Related
I'm trying to push a new value in the store's state. It works fine the first time I click on the button "Add item", but the second time I got the following error: "state.basket.push is not a function". I configure the action to console log the state and got the following results:
1st click: {...}{basketItems: Array [ "44" ]}
2nd click: Object {basketItems: 0 }
Why the variable type is changing from array to an int?
Here is the code for the rendered component:
function Counter({ basketItems,additem }) {
return (
<div>
<button onClick={additem}>Add item</button>
</div>
);
}
const mapStateToProps = state => ({
basketItems: state.counterReducer.basketItems,
});
const mapDispatchToProps = dispatch => {
return {
additem: ()=>dispatch({type: actionType.ADDITEM, itemName:'Dummy text' }),
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
And the reducer looks like this:
import {ADDITEM} from "../actions/types";
const initialState = { basket: [], };
export default function reducer(state = initialState, action) {
switch (action.type) {
case ADDITEM:
console.log(state);
// let newBasket = state.basket.push('44');
return {
...state,
basket: state.basket.push('44')
};
default:
return state;
}
}
I'm copying the state before updating the basket to prevent weird behaviors.
There's two problems here:
state.basket.push() mutates the existing state.basket array, which is not allowed in Redux
It also returns the new size of the array, not an actual array
So, you're not doing a correct immutable update, and you're returning a value that is not an array.
A correct immutable update here would look like:
return {
...state,
basket: state.basket.concat("44")
}
Having said that, you should really be using our official Redux Toolkit package, which will let you drastically simplify your reducer logic and catch mistakes like this.
There are two event listeners that are apparently useful when to monitor the network status:
1. window.addEventListener('online', console.log('Online'));
2. window.addEventListener('offline', console.log('Offline'));
But I am not sure where to register and use them. When I use them within componentDidMount, there is no use because monitoring would happen only if the component is mounted. I want to monitor the network status in one place and use it across the application. For this, dispatching the network status in redux would be more helpful. But the problem is where to listen for this events.
Simplified example with class components:
// In your main App component
componentDidMount() {
window.addEventListener('online', () => this.props.setConnectivity('online'));
window.addEventListener('offline', () => this.props.setConnectivity('offline'));
// You don't need to worry about removeEventlistener if your main App component never unmounts.
}
// Action
const setConnectivity = (status) => ({
type: 'SET_CONNECTIVITY',
payload: status === 'online'
})
// Reducer
const connectivityReducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_CONNECTIVITY':
return {
...state,
isOnline: action.payload
};
}
};
// To let a component know about the connectivity status, simply use the flag from state:
const mapStateToProps = (state) => ({
isOnline: state.connectivity.isOnline
});
// To react to status changes in any other component:
componentDidUpdate(prevProps) {
const { isOnline } = this.props;
if (!prevProps.isOnline && isOnline) {
// We went online
} else if (prevProp.isOnline && !isOnline) {
// We went offline
}
}
I suggest to use this very new library
Then you can use useNetworkStatus to get what you wanted, something like:
import React from 'react';
import { useNetworkStatus } from 'react-adaptive-hooks/network';
const MyComponent = () => {
const { effectiveConnectionType } = useNetworkStatus();
let media;
switch(effectiveConnectionType) {
case 'slow-2g':
media = <img src='...' alt='low resolution' />;
break;
case '2g':
media = <img src='...' alt='medium resolution' />;
break;
case '3g':
media = <img src='...' alt='high resolution' />;
break;
case '4g':
media = <video muted controls>...</video>;
break;
default:
media = <video muted controls>...</video>;
break;
}
return <div>{media}</div>;
};
effectiveConnectionType - will change if the network changes, `
navigator.connection.addEventListener('change', func) - this will also get trigger on online and on offline
Here is a working example of an online / offline status indicator in React+Redux.
The magic comes from having a component that adds an event listener for the online event in the component's componentDidMount event. Then it dispatches an action via Redux to update the online status in the store. A container component (MainContainer in this example) can map the global store state to props, and any presentational component can update in response to this property changing.
By the way, if you want to test this online indicator on this page with Chrome: run the snippet, then launch the Dev Tools (F12), then toggle the device toolbar (Ctrl+Shift+M) (or 2nd icon in the top left of the dev tools window), switch to Responsive layout, and toggle the Online / Offline state in the bar (as shown in this picture) as desired:
function rootReducer(currentState, action) {
currentState = currentState || { status: true }; // Initial State
switch (action.type) {
case 'SET_ONLINE_STATUS':
return { ...currentState, status: action.status };
default:
return currentState; // Always return the state
}
}
// Action Creators:
function setOnlineStatus(status) {
return { type: 'SET_ONLINE_STATUS', status };
}
// Create Store
var rootStore = Redux.createStore(rootReducer);
// Map state and dispatch to props
function mapStateToProps(state) {
return {
status: state.status
};
}
function mapDispatchToProps(dispatch) {
return Redux.bindActionCreators({
setOnlineStatus: setOnlineStatus
}, dispatch);
}
// Connection indicator pure functional presentational component
var ConnectionIndicator = (props) => {
return (<div>You are: {props.status ? 'online' : 'offline'}</div>);
};
var Main = React.createClass({
render: function () {
return (<div>
<ConnectionIndicator status={this.props.status} />
</div>);
}
});
var OnlineWatcher = React.createClass({
render() { return null; },
componentDidMount() {
window.addEventListener('online', () => {
this.props.setOnlineStatus(true);
})
window.addEventListener('offline', () => {
this.props.setOnlineStatus(false);
})
}
});
// Container components (Pass props into presentational component)
var MainContainer = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(Main);
var OnlineWatcherContainer = ReactRedux.connect(mapStateToProps,
mapDispatchToProps)(OnlineWatcher);
// Top-Level Component
var App = React.createClass({
render: function () {
return (
<div>
<MainContainer />
<OnlineWatcherContainer />
</div>
);
}
});
// Render to DOM
var Provider = ReactRedux.Provider; // Injects store into context of all descendents
ReactDOM.render(
<Provider store={rootStore}>
<App />
</Provider>,
document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.5.2/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.5/react-redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.13.0/polyfill.js"></script>
<div id="container">
<!-- Yield to React -->
</div>
NOTE: In production code, you should also unsubscribe from the online and offline events in the componentWillUnmount function.
I used this fiddle as a starting point.
Before all, you should have redux store with actions and reducers.You are able to connect every component you want to store and use this source of truth. You can find documentations and exampel here https://react-redux.js.org/introduction/quick-start
Actually you can find example of using online status Integrating Navigator.onLine into React-Redux
when using hooks, this simple solution would work https://github.com/saulpalv/useOnline
npm i #saulpalv/useonline
use like this
import type { FC } from 'react';
import { useOnline } from '#saulpalv/useonline';
export const DialogNetwork: FC = ()=> {
const isOnline = useOnline();
return (
<Dialog show={!isOnline}>
You are not online
</Dialog >
);
});
I am pretty new guy to redux.I am little bit confused that how to change this data without mutating it.
my data structure like this
{
postOwnwerName:"",
postTags:[],
photos:[
{
id:dc5c5d7c5s,
caption:null
},
{
id:dccccccc5s,
caption:null
}
],
}
Whent this function fires
this.props.updateCaption(id,caption)
my Reducer should set data according to the id in the data structure without mutating it
import * as types from '../Actions/actionTypes';
const initialState = {
postOwnwerName:"",
postTags:[],
photos:[],
};
export default function uploadReducer(state=initialState,action) {
switch (action.type){
case types.SETPHOTOSTATE:
return Object.assign({},state,{ photos:state.photos.concat(action.payload) });
case types.SETCAPTIONTEXT:
//this method is not working
return (
Object.assign({},state,state.photos.map((data) => {
if(data.id == action.payload.id){
return Object.assign({},data,{caption:action.payload.caption})
}else{
return data
}
}))
)
default:
return state
}
}
if the state object has many level it will be difficult to change state without mutating. In those scenario I first copy the state deeply and then make changes to the new state and return new state. See here Cloning object deeply. You can try this once.
export default function uploadReducer(state=initialState,action) {
let newState = JSON.parse(JSON.stringify(state));
switch (action.type){
case types.SETPHOTOSTATE:
newState.photos.push(action.payload);
return newState;
case types.SETCAPTIONTEXT:
newState.photos.map((data) => {
if(data.id == action.payload.id){
data.caption = action.payload.caption;
}
});
return newState;
default:
return newState;
}
}
I can't seem to reset the default state; how do I do that? I tried this but all it does is add state to state and calls it undefined.
const initialState = {
name: null,
coins: 0,
image: null,
};
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case types.ADD_GROUP_COINS:
return {
...state,
coins: state.coins + action.coins
};
case types.DELETE_GROUP:
return {
state: undefined
};
default:
return state;
}
}
To reset the state to the initialState, simply return initialState, like this:
case types.DELETE_GROUP:
return initialState;
Remember that in the reducer, the object that you return is the new state. You don't need to wrap it in another object, like this:
return {
state: ...
}
That will give you a property called state in your state, which isn't at all what you need.
From the documentation:
The reducer is a pure function that takes the previous state and an action, and returns the next state.
It's easy to get confused about this, so be careful! Take a look at your default case if you're still not quite sure: you're returning the old state variable as the new state, not { state: state } which you are essentially doing with the current DELETE_GROUP handler.
Currently in my React-Redux app, I would like to use React to set the state of show to either false, or true. When set to true, the app will initialize. (There are multiple components, so it would make sense to do this using react/redux.)
However, my current issue is that even though I have connected my app using react redux, and my store using provider, the dispatch action will be called, without updating the store(I am using redux dev tools to double check, as well as in app testing).
I have attached the code I feel is relevant, however, the entire code base is available as a branch specifically made for this question here. I have spent quite sometime on this(actually an understatement) and any contributions would be greatly appreciated.
Component Relevant Code
hideBlock(){
const{dispatch} = this.props;
dispatch(hideBlock);
}
return(
<div className = "works">
<button id="show-block" type="button" className="show-hide-button" onClick={this.showBlock}>show</button>
<button id="hide-block" type="button" className="show-hide-button" onClick={this.hideBlock}>Hide</button>
</div>
);
function mapStateToProps(state) {
const {environment} = state;
return{
environment
}
}
export default connect(mapStateToProps)(Form);
Action
import * as types from "../constants/ActionTypes";
export function showBlock(show) {
return {
type: types.SHOW,
show: true
};
}
export function hideBlock(hide) {
return {
type: types.HIDE,
show: false
};
}
Reducer
import * as types from "../constants/ActionTypes";
const initialState = {
show: false
};
export default function environment(state = initialState, action) {
switch(action.type) {
case types.HIDE:
return Object.assign({}, state, {
type: types.HIDE
});
case types.SHOW:
return Object.assign({}, state, {
type: types.SHOW
});
default:
return state;
}
}
Thank you, and once again any help is very much so appreciated.
So, I asked a co-worker for help and it turns out that I was returning my action as an object instead of a function. So, for instance, changing the following code:
hideBlock(){
const{dispatch} = this.props;
dispatch(hideBlock);
}
to
hideBlock(){
const{dispatch} = this.props;
//change hideBlock to hideBlock()
dispatch(hideBlock());
}
solved the issue. Thanks Andrew!
It looks like state.show is set in initialState but never modified in any of the cases inside the reducer. The action has show: true, but the reducer never uses this to update the state.
Here's a simplified reducer that updates state.show based on the action's show field:
export default function environment(state = initialState, action) {
switch(action.type) {
case types.HIDE:
case types.SHOW:
return Object.assign({}, state, {
show: action.show
});
default:
return state;
}
}
Alternatively, because action.show and action.type have the same data, you could remove show from the actions and rely on the action type:
export default function environment(state = initialState, action) {
switch(action.type) {
case types.HIDE:
return Object.assign({}, state, {
show: false
});
case types.SHOW:
return Object.assign({}, state, {
show: true
});
default:
return state;
}
}