React-Redux component state - reactjs

tl;dr I'm trying to save initial state inside a sub-container component but it gets updated to the new values every time the Redux store gets updated. I probably missed something in configuration and I need help to sort things out.
index.tsx
const store = createStore(reducers, loadedState, enhancer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById(containerId)
)
AppProps.ts
function mapStateToProps(state: StoreState){
return{
... // app props
userDetails: state.userDetails // array of objects fetched by id
}
}
function mapDispatchToProps(dispatch: ReactRedux.Dispatch<actions.AppActions>){
return{
... // app methods
detailsUpdate: (props: UpdateProps) => (dispatch(actions.detailsUpdate(props: UpdateProps)))
}
}
ReactRedux.connect(mapStateToProps, mapDispatchToProps)(App)
Actions.ts
function detailsUpdate(props: UpdateProps): UpdateUserDetails {
return {
type: UPDATE_USER_DETAILS,
props: { ... }
}
}
Reducers.ts
export function reducers(state: StoreState, action: actions.AppActions): StoreState {
let newState: StoreState = {...state};
switch (action.type) {
case actions.UPDATE_USER_DETAILS:
... // reducer code
break;
case actions.UPDATE_PRODUCTS:
... // reducer code
break;
return newState;
}
App.tsx
const App = (allProps: IAppProps, state: StoreState) => {
<UserDetailsContainer
id="generalDetails"
userDetails={allProps.userDetails.byId}
detailsUpdate={allProps.detailsUpdate}
/>
}
UserDetailsContainer.tsx 1
class UserDetailsContainer extends
React.Component<UserDetailsContainerProps, UserDetailsState> {
constructor(props: UserDetailsContainerProps) {
super(props);
this.state = {
userData: props.userDetails[props.id]
}
}
render(){
<input type="text"
value={this.props.userDetails[this.props.id].address}
/>
}
}
detailsUpdate triggers UPDATE_USER_DETAILS action and reducer updates store state with new value. Now, UserDetailsContainer receives updated version of userDetails from the store which is fine for displaying new value in <input type="text"> element.
However, this.state gets updated with new value which I expect shouldn't happen as constructor should be called only once (and is). This prevents me from referencing initial value in case I need it for reset or other.
Please ask for any missing information and/ or clarification and ignore any typos as the app works without errors otherwise.
1 Component usually renders another presentational component for <input type="text">which I omitted here for brevity.
Thanks!

The following only makes a shallow copy of state object.
let newState: StoreState = {...state};
So if you assign
this.state = {
userData: props.userDetails[props.id]
}
And then modify the array inside your reducer, you will also modify the component's state since it references the same object.
This also goes against the concept of redux - reducer should not mutate it's arguments.
Note that this exact mistake is highlighted in the redux docs: https://redux.js.org/recipes/structuring-reducers/immutable-update-patterns#common-mistake-2-only-making-a-shallow-copy-of-one-level

Related

Cannot pass data between components in React/Redux

I'm new to React and am trying to build an app which shuffles football players into two teams and am having difficulty with passing data from one component to another. I have redux and react-redux installed.
My problem is that once 10 names have been inputted (which I am calling numbersReached), the button to submit the addPlayers form should be disabled so no more players can be submitted.
I have tried passing a state value, numbersReached, into my AddPlayers component, but it is not being imported correctly - it is showing in console.log as undefined.
My code so far:
'initialState.js'
export const initialState = {
playersList: [],
shuffledList: [],
teamA: [],
teamB: [],
numbersReached: false
};
export default initialState;
'src\components\NumbersReached\index.js':
import { connect } from "react-redux";
import NumbersReached from "./NumbersReached";
const mapStateToProps = (state) => {
return {
...state,
numbersReached: state.playersList.length >= 10 // Once state.playersList.length>=10 numbersReached should = true. (10 is for five-a-side)
};
};
export default connect(mapStateToProps)(NumbersReached);
src\components\NumbersReached\NumbersReached.js:
import React from "react";
const NumbersReached = ({ numbersReached }) => (
<div>
{numbersReached ? "Numbers Reached" : null}
</div>
);
export default NumbersReached;
'src\components\AddPlayer\index.js':
import { connect } from "react-redux";
import AddPlayer from "./AddPlayer";
import { addPlayer } from "../../data/actions";
const mapStateToProps = (state) => {
return {
playerName: state.playerName,
numbersReached: state.numbersReached
};
};
const mapDispatchToProps = (dispatch) => {
return {
handleSubmit: (data) => dispatch(addPlayer(data)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AddPlayer);
'src\components\AddPlayer\AddPlayer.js'
import React, { Component } from 'react';
class AddPlayer extends Component {
constructor(props) {
super(props);
this.state = {
playerName: props.playerName,
numbersReached: props.numbersReached
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
};
handleChange(e) {
this.setState({ playerName: e.currentTarget.value });
}
handleSubmit(e) {
e.preventDefault();
this.props.handleSubmit({ ...this.state });
}
render() {
return (
<React.Fragment>
<form className="entry-form" onSubmit={this.handleSubmit}>
<input
placeholder="Enter a player's name"
className="player-input"
type="text"
onChange={this.handleChange}
/>
<button
type="submit"
className="player-submit"
disabled={this.state.numbersReached} // Does not disable the button as this.state.numbersReached is undefined
>
Add a player
</button>
</form>
</React.Fragment>
)
}
};
export default AddPlayer;
Help is very much appreciated!
The problem you're likely having is that you're not actually using the value of numbersReached in your Redux store. Instead, you're using a numbersReached variable inner to your AddPlayer component' state.
<button
type="submit"
className="player-submit"
disabled={this.state.numbersReached}
>
To use the numbersReached you defined in mapStateToProps, you should access to this.props.numbersReached instead of this.state.numbersReached. The props you inherit from Redux will automatically be updated when the action addPlayer is dispatched and the Redux store changes.
The value for your this.state.numbersReached, however, won't change unless you call setState anywhere in your component. Right now, you're only initializing that value on the constructor:
class AddPlayer extends Component {
constructor(props) {
super(props);
this.state = {
playerName: props.playerName,
numbersReached: props.numbersReached
};
};
}
When you do this, you're creating a new numbersReached variable in your component' state with the value that the numbersReached property in your Redux store had at the time your component is built. As it doesn't have value when the constructor runs, this.state.numbersReached is undefined.
Doing numbersReached: props.numbersReached does not suscribe you to all of the changes to that property; it just assigns numbersReached the value props.numbersReached initially had. That's why, no matter how many players you add, it'll keep been undefined even if players have been succesfully added to your store.
you are duplicating redux state at AddPlayer state. duplicate state is bad practice and should be avoided. secondly, they are different states, redux'state and react component's state. as it is this.state.numbersReached receives the first value on mounting but is not not updated anywehere after.
Also it is weird at your state playerName: props.playerName. first, the fact that you dont have at your redux a piece of state called playerName. Second, the most revelant, it's a component that create players and adds to your playersList. you should better initialize that state as an empty string.
btw, I refactored a little your component to reduce code. if you declare your functions as arrow functions you dont need to bind them at constructor. and you dont need to declare your state at constructor anymore. But if you want to keep the original for consistency, or personal preference go ahead, that's ok also :) .
after all that, your component would look something like:
import React, { Component } from 'react';
class AddPlayer extends Component {
state = {
playerName: ''
};
handleChange = (e) => {
this.setState({ playerName: e.currentTarget.value });
}
handleSubmit = (e) => {
e.preventDefault();
this.props.handleSubmit({ ...this.state });
}
render() {
return (
<React.Fragment>
<form className="entry-form" onSubmit={this.handleSubmit}>
<input
placeholder="Enter a player's name"
className="player-input"
type="text"
onChange={this.handleChange}
/>
<button
type="submit"
className="player-submit"
disabled={this.props.numbersReached}
>
Add a player
</button>
</form>
</React.Fragment>
)
}
};
export default AddPlayer;
a last note, you have numbersReached state, but at your mapStateToProps you set based on a custom function, not on your numbersReached state.
if you want to keep numbersReached state on your redux state, you should handle that logic state.playersList.length >= 10 at your reducers to update properly there, and not at your mapStateToProps. though you could remove from your redux state numbersReached altogether given it's derived from other pieces from your state.

Is there any function in the t-redux library that acts like the official connect function in redux?

I need to connect various components to the state managed by t-redux, but t-redux seems to have only a withState() function that accepts only reducers and some initial state to work on. Hence every component seems to receive a "brand new state".
The library is this one: https://www.npmjs.com/package/t-redux
This is the official example
// import the needed modules
import {withState, dispatcher, buildReducer} from 't-redux'
// this is a PORC (Plain Old React Component)
class MyCounter extends React.Component {
constructor() {
super()
this.plusOne = this.plusOne.bind(this)
}
plusOne() {
// Dispacth the action (the content is optional)
dispatcher.dispatch({type: 'PLUS_ONE', content: this.props.counter})
}
render() {
return (
<div>
<div>Click count: {this.props.counter}</div>
<button onClick={this.plusOne}>Add 1</button>
</div>)
}
}
// Build the reducers as a map ACTION:(state, action) => state
const reducers = buildReducer({
'PLUS_ONE': (state, action) => ({counter: state.counter + 1})
})
// Define the initial state
const INITIAL_STATE = { counter: 0 }
// export the wrapped component passing the reducers and the initial state
export default withState([reducers], INITIAL_STATE)(MyCounter)
If you look at the implementation of t-redux's withState, you'll see that each Higher Order Component contains its own state. The particular segment is here:
constructor(props) {
super(props)
this.state = { innerState: initialState }
}
componentWillMount() {
this.regId = dispatcher.register(action => {
const nextState = combineReducers(reducers, this.state.innerState, action)
this.setState({innerState: nextState})
})
}
componentWillUnmount() {
dispatcher.unregister(this.regId)
}
Essentially, it is using React's component state to store a redux pattern state. Since it is implemented this way, there isn't a way to have a single store for all your components.

react props not updating with redux store

I've always used react-redux connect to configure props but I need to use a react Component to use lifecycle methods. I'm noticing that my props that I'm grabbing from the store seem to be static and they do not update as the store updates.
Code:
class AlertModal extends Component {
title
isOpen
message
componentDidMount() {
const { store } = this.context
const state = store.getState()
console.log('state', state)
console.log('store', store)
this.unsubscribe = store.subscribe(() => this.forceUpdate())
this.title = state.get('alertModal').get('alertModalTitle')
this.isOpen = state.get('alertModal').get('isAlertModalOpen')
this.message = state.get('alertModal').get('alertModalMessage')
this.forceUpdate()
}
componentWillUnmount() {
this.unsubscribe()
}
updateAlertModalMessage(message) {
this.context.store.dispatch(updateAlertModalMessage(message))
}
updateAlertModalTitle(title) {
this.context.store.dispatch(updateAlertModalTitle(title))
}
updateAlertModalIsOpen(isOpen) {
this.context.store.dispatch(updateAlertModalIsOpen(isOpen))
}
render() {
console.log('AlertModal rendered')
console.log('AlertModal open', this.isOpen) <======= stays true when in store it is false
return (
<View
How do I set up title, isOpen, and message so they reflect the store values at all times?
It should be something like this. In your Confirmation component:
const mapStateToProps = (state) => {
return { modalActive: state.confirmation.modalActive };
}
export default connect(mapStateToProps)(Confirmation);
In your reducer index file, is should be something like this:
const rootReducer = combineReducers({
confirmation: ConfirmationReducer
});
I believe you have your own reducer file called ConfirmationReducer here. It should be something like this.
import { ON_CONFIRM } from '../actions';
const INITIAL_STATE = {modalActive: true};
export default function(state = INITIAL_STATE, action) {
console.log(action);
switch (action.type) {
case ON_CONFIRM:
return { ...state, modalActive: action.payload };
}
return state;
}
Make sure you write your own action creator to create an action with the above type and relevant payload of boolean type.
Finally you should be able to access the property from the store inside your Confirmation component like this:
{this.props.modalActive}
You have not posted entire code, so it makes very difficult to give a solution to the exact scenario. Hope this helps. Happy coding!
For me the problem was that I was assigning this.props.myObject to a variable which wasn't deep cloned so I fixed it by using
let prev = Object.assign({}, this.props.searchData)
What I was doing
let prev = this.props.searchData
So I was disturbing the whole page.Seems quiet noob on my part.
this may help you
componentWillReceiveProps(nextProps) {
console.log();
this.setState({searchData : nextProps.searchData})
}

MobX: rerender after assign

Good day!
I have an parent component:
#observer
class ToDos extends Component {
componentWillMount() {
this.state = new State();
this.onClearAllCompleted = this.onClearAllCompletedHandler.bind(this);
}
onClearAllCompletedHandler() {
this.state.clearAllCompleted();
}
render() {
const todos = this.state.todos;
return (
<Provider todos={todos}>
{this.props.children}
<ClearAllButton onClick={this.onClearAllCompleted} />
</Provider>
);
}
}
And state class for it:
class TodosState {
#observable todos = [
{ title: 'Sleep', completed: true },
{ title: 'Sleep more', completed: false },
{ title: 'Sleep more than before', completed: false }
];
#action
clearAllCompleted() {
this.todos = this.todos.filter(todo => !todo.completed);
}
}
When i try to clear all completed todos, it clears they with warning in browser console: MobX Provider: Provided store 'todos' has changed. Please avoid replacing stores as the change might not propagate to all children.
After this nothing happens: i have old rendered html ;(
So, i think that childrens has observable object of todos that references to one object and after assign in state i have different ref. Childs dont know about this, and their observable doesn't changed at all. So, what i can do in this case?
The issue is in render method - on each re-render you pass new todos into Provider component. Provider component is tricky component which needs always the same props, but you pass different todos array each time.
Corrected code: pass the whole state object into Provider (this.state object is always the same in your example, just as Provider wants)
render() {
return (
<Provider store={this.state}>
{this.props.children}
<ClearAllButton onClick={this.onClearAllCompleted} />
</Provider>
);
}
By the way I recommend you replace componentWillMount() with constructor(). Constructor is better place for store initialization. Especially in next versions of React (16.0+) componentWillMount() might be called several times for the same instance before actual mount.

Adding Redux to an existing React app

I've been working on a React app and have gotten to a point where I'll need Redux to handle some aspects of it.
After reading a bunch of tutorials, I'm fairly stuck on how to make my "smarter" components "dumber" and move functions into my actions and reducers.
So, for example, one aspect of the app is more of a to-do list style.
One of my classes starts like this:
export default class ItemList extends React.Component {
constructor() {
super();
this.state = { items: [],
completed: [],
};
this.addItem = this.addItem.bind(this);
this.completeItem = this.completeItem.bind(this);
this.deleteItem = this.deleteItem.bind(this);
}
addItem(e) {
var i = this.state.items;
i.push({
text: this._inputElement.value,
paused: false,
key: Date.now()
});
this.setState({ items: i });
e.preventDefault();
this._inputElement.value = '';
this._inputElement.focus();
}
completeItem(e) {
this.deleteItem(e);
var c = this.state.completed;
c.push({
text: e.target.parentNode.parentNode.getElementsByClassName('item-name')[0].innerHTML,
paused: false,
key: Date.now()
});
this.setState({ completed: c });
}
deleteItem(e) {
var i = this.state.items;
var result = i.filter(function(obj) {
return obj.text !== e.target.parentNode.parentNode.getElementsByClassName('item-name')[0].innerHTML;
});
this.setState({ items: result });
}
// ... more irrelevant code here ...
// there's a function called createTasks that renders individual items
render() {
var listItems = this.state.items.map(this.createTasks);
return <div className="item-list">
<form className="form" onSubmit={this.addItem}>
<input ref={(a) => this._inputElement = a}
placeholder="Add new item"
autoFocus />
<button type="submit"></button>
</form>
{listItems}
</div>;
}
}
So, as you can see, it's very logic-heavy. I've started adding Redux by adding a <Provider> in my index file, and made a basic reducers file that is fairly empty so far:
import { combineReducers } from 'redux';
const itemList = (state = {}, action) => {
};
// ... other irrelevant reducers
const rootReducer = combineReducers({
itemList,
// ...
});
export default rootReducer;
...and I've made an actions file that doesn't have much in it yet either.
I've been struggling to figure out:
Most actions I've seen examples of just return some kind of JSON, what do I return in the reducer that uses that JSON that my component can use?
How much of my component logic is reusable, or should I just forget it? What is the best way to go about this to reuse as much code as I've written as possible?
First of all you need to understand the overall picture of how redux works with react.
Before coming to that lets first understand what are smart components and dumb components.
Smart Components
All your code logic needs to be handled here
They are also called containers.
They interact with the store(aka state management) to update your components.
Dumb Components
They just read props from your containers and render you components
This is just the UI view and should not contain any logic.
All styling/html/css comes in your dumb components.
Here is an amazing piece of article which you can go through to understand smart and dumb components if you still have doubts.
Ok, now lets try understanding how redux works:-
Your smart components(aka containers) interact with your redux store
You fire actions from your containers.
Your actions call your apis
The result of your action updates the store through a reducer
You containers read the store through mapStateToProps function and as soon as value in store changes it updates your component.
Now lets consider your todo example
TodoListContainer.js
class TodoListContainer extends Component {
componentWillMount () {
// fire you action action
}
render () {
return (
<Todos todos=={this.props.todos} />
)
}
}
function mapStateToProps(state) {
const {todos} = state;
return {
todos;
}
}
export default connect(mapStateToProps)(TodoListContainer)
TodoList.js
class TodoList extends Component {
renderTodos() {
return this.props.todos.map((todo)=>{
return <Todo todo={todo} key={todo.id} />
})
}
render () {
return () {
if (this.props.todos.length === 0) {
return <div>No todos</div>
}
return (
<div>
{this.renderTodos()}
</div>
)
}
}
}
export default class TodoList
Todo.js
class Todo extends Component {
render () {
return (
<div>
<span>{this.props.todo.id}</span>
<span>{this.props.todo.name}</span>
</div>
)
}
}
Reducer
export default function todos(state={},action) {
switch (action.type) {
case 'RECEIVE_TODOS':
return Object.assign(state,action.todos);
}
}
action
function fetchTodos() {
return(dispatch) => {
axios.get({
//api details
})
.then((res)=>{
dispatch(receiveTodos(res.todos))
})
.catch((err)=>{
console.warn(err)
})
}
}
function receiveTodos(todos) {
return {
type: 'RECEIVE_TODOS',
todos
}
}
Now if you have read redux documentation you would see that actions return objects then how would i call my api there which returns a function instead of an object. For that I used redux thunk about which you can read here.
I gave you an example in which you can fetch todos. If you want to do other operations like deleteTodo, addTodo, modifyTodo then you can do that in appropriate components.
DeleteTodo - you can do in TodoListContainer.
AddingTodo - you can do in TodoListContainer.
Changing State(completed/Pending) - you can do in TodoListContainer.
ModifyingTodo - you can do in TodoContainer.
You can also check out here for a detailed example, but before that I would say just should go through basics of redux which you can find here
P.S: I wrote code on the fly so it might not work properly but it should work with little modification.

Resources