React components lifecycle, state and redux - reactjs

I would like to use redux to store the state of my whole react application, however I am stuck with a particular case:
what to do with redux when the component needs a local state, modified by lifecycle methods like componentDidUpdate or componentDidMount ?
Example of a react component to contain "cards" arranged by isotope layout library:
componentDidMount() {
let container = ReactDOM.findDOMNode(this);
if (! this.state.isotope) {
this.setState({ isotope: new Isotope(container, {itemSelector: '.grid-item', layoutMode: 'masonry'})});
}
}
componentDidUpdate(new_props, new_state) {
if (new_state.items_list != this.state.items_list) {
if (this.state.isotope) {
this.state.isotope.reloadItems();
this.state.isotope.layout();
this.state.isotope.arrange();
}
}
}
Is there a way to remove the local state in this component and to use redux instead ? I can't see how to do it

Put your items_list in your redux state. Your reducer might look like this:
const initialState = {
items: []
};
export function myReducer(state = initialState, action) {
switch (action.type) {
case 'SET_ITEMS':
return Object.assign({}, state, {
items: action.items
});
}
return state;
}
Or for something a little more complex:
const initialState = {
items: []
};
export function myReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_ITEM':
return Object.assign({}, state, {
items: [ ...state.items, action.item ]
});
case 'REMOVE_ITEM':
return Object.assign({}, state, {
items: [
...state.items.slice(0, action.index),
...state.items.slice(action.index + 1)
]
});
}
return state;
}
Once you've configured your store and Provider (see the Redux docs), set up your "smart component" like so:
function mapStateToProps(state) {
return {
items: state.items
}
}
function mapDispatchToProps(dispatch) {
const actions = bindActionCreators(actionCreators, dispatch);
return {
addItem: actions.addItem,
removeItem: actions.removeItem
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Root);
Now your items and actions are props to your Root component. If your items live in a lower order component, simply pass them down the tree as props.
I hope that gives you the general idea. With Redux what you'll find that your React components will use state a lot less and props a lot more.
One more thing...
This might be a minor matter, but I urge you NOT to store your isotope object on the component state. (Regardless of whether or not you use Redux.) The isotope object isn't really a piece of state, it's your view. In React, a component updates in response to a change in state. But your componentDidUpdate does the reverse: it changes the state in response to a component update.
As an alternative, simply store it on the object itself. i.e.
componentDidMount() {
const container = ReactDOM.findDOMNode(this);
this.isotope = new Isotope(container, {
itemSelector: '.grid-item',
layoutMode: 'masonry'
});
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.items !== this.props.items) {
this.isotope.reloadItems();
this.isotope.layout();
this.isotope.arrange();
}
}
(Whilst normally I would recommend against against using these sort of instance variables in React, DOM manipulation libraries like Isotope are a worthy exception.)

Related

I can't figure out how to use visibilityFilters in react redux todo app

I have a todo app that does all 4 crud operations but I can't filter them based on their current status here's the app on codesandbox.
import { SET_VISIBILITY_FILTER } from "../actionTypes";
const initialState = {
filters: ["SHOW_ALL"]
};
const visibilityFilter = (state = initialState, { type, payload }) => {
switch (type) {
case SET_VISIBILITY_FILTER:
return { payload };
default:
return state;
}
};
export default visibilityFilter;
Any explanations will be appreciated.
I have also checked other react redux todo app github repos but most of them are old and it didn't look like they were writing in the best possible way, so I am trying to find a better way (and so far failing at it)
A few issues
filters is an array in the initial state, but you send single values there after in your action, and you also use it a single value when filtering with it.
you expect payload in your reducer but the data you dispatch does not wrap things in payload
dispatch({
type: SET_VISIBILITY_FILTER,
filter
});
in continuation to the above you should use the already defined action setFilter for setting a filter, which correctly wrap the data in a payload property.
fixing these 3 issues, you get https://codesandbox.io/s/problems-with-redux-forked-hv36h which is working as intended.
What you are doing is an anti-pattern when you mutate the redux state variable inside the component like this:
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case "SHOW_ALL":
return todos;
case "SHOW_COMPLETED":
return todos.filter((t) => t.completed);
case "SHOW_ACTIVE":
return todos.filter((t) => !t.completed);
default:
return todos;
}
};
Instead what you should do, listen to the SET_VISIBILITY_FILTER action on toDoReducer.js:
//import SET_VISIBILITY_FILTER action
case SET_VISIBILITY_FILTER:
let toDoClone = [...state.todos]
//if(filter = something)
toDoClone.filter(t => //your condition)
return {
...state,
todos: toDoClone
}

Redux MapDispatchToProps not functioning

So I'm new to Redux and I'm trying to get this base model working so I can quickly work on a small personal project, I set everything up and have no errors but I'm trying to test and my function doesn't work so I was hoping someone could point out what I've missed.
I've followed multiple different tutorials and each has a different approach so that has me lost a bit so I apologize for that.
My store.js looks like so
import rootReducer from "./reducers";
import thunk from "redux-thunk";
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
I've used a combineReducers in my index.js in reducers folder and the auth: points to the authReducer.js file, which is this
const INIT_STATE = {
email: "",
password: "",
isLoggedIn: "false"
};
export default (state = INIT_STATE, action) => {
switch (action.type) {
case IS_LOGGED_IN_CHANGE:
console.log(action);
return {
isLoggedIn: action.value
};
default:
return state;
}
};
Now What I'm aiming for is to have a button that changes that "IsLoggedIn" initial state to a true string instead of a false, I've went into my actions folder and made an authActions.js which looks like so
import { IS_LOGGED_IN_CHANGE } from "../actions/types";
import store from "../store";
export const isLoggedInChange = value => {
return dispatch => {
dispatch({
type: IS_LOGGED_IN_CHANGE,
value
});
};
};
And Finally I want to show you my component page which is showing all this, It's looking like so
import { connect } from "react-redux";
import styles from "./Landing.module.css";
import { isLoggedInChange } from "../../actions/authActions";
class Landing extends Component {
makeTrue = () => {
isLoggedInChange("true");
};
constructor(props) {
super(props);
this.state = {
email: "",
password: ""
};
}
render() {
return (
<div className={styles.background}>
<button onClick={this.makeTrue}>MAKE TRUE</button>
{this.props.isLoggedIn}
</div>
);
}
}
const mapStateToProps = state => ({
isLoggedIn: state.auth.isLoggedIn
});
const mapDispatchToProps = dispatch => ({
isLoggedInChange: value => dispatch(isLoggedInChange(value))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Landing);
Can you tell if I dropped anything making this? why is the button not changing the store state? TIA
Two problems here. You're calling your action creator directly not props.isLoggedInChange
makeTrue = () => {
this.props.isLoggedInChange("true");
};
And you need to spread the old state inside your action
case IS_LOGGED_IN_CHANGE:
console.log(action);
return {
...state,
isLoggedIn: action.value
};
Isn't the point of my mapDispatchToProps to be able to use the function right away as I was doing
Yes, the problem is mapDispatchToProps inject a function (or multiple functions) wrapped in dispatch into your props.
import { actionCreator } from './actions
const mapDispatchToProps = dispatch =>({
actionCreator : () => dispatch(actionCreator)
})
Now you have two actionCreator, one globally available in the scope (which is your action creator) and props.actionCreator which is the original action creator wrapped in dispatch. So when you call actionCreator() from inside your component it won't throw any errors (cause there is a function named actionCreator in the scope, but you will be calling the wrong function, the right one is located at props.actionCreator.
Why do I need to spread the state?
A reducer is a pure function which receives a state and action and returns the new state. When you just return
return {
isLoggedIn : true
}
You're actually overwriting the original state (which contains other properties), so first you need to spread the original state to maintain it's structural integrity and them overwrite the properties you want
return{
...state,
isLoggedIn : !state.isLoggedIn
}
Redux state is immutable so you need to return a brand new instance of state, change your reducer state to the below.
export default (state = INIT_STATE, action) => {
switch (action.type) {
case IS_LOGGED_IN_CHANGE:
console.log(action);
return Object.assign({}, state, {
isLoggedIn: action.value
});
default:
return state;
}
};
The key difference there being the
return Object.assign({}, state, {
isLoggedIn: action.value
});
Object.assign in the way I'm using it here combines the state object into a brand new object. Check out immutability within redux reducers and I'd recommend adding redux-immutable-state-invariant as a dev package, it can detect when you're directly modifying state and help point out errors like this
Return the state with the new value for isLoggedIn. Use the reducer like this:
case IS_LOGGED_IN_CHANGE:
console.log(action);
return {
...state,
isLoggedIn: action.value
};

Setting deeply nested object in Redux reducer not propagating updates to my component

I am trying to set a deeply nested object in my react.js/redux app via a reducer, but the object changes are not propagating down to my component. How should I update and mapStateToProps an object like this?
I have tried ...ing my action's payload in my reducer, mapping a higher level object in my mapStateToProps, and flattening the object I map to my components props.
My initial state:
const initialState = {
myObject : {
prop1 : {
prop2 : {
array1 : [],
array2: [],
array3: []
}
}
}
}
My reducer:
const app = (state = initialState, action) => {
switch(action.type) {
case SET_DATA:
return {
...state,
myObject: action.payload
};
} default:
return state;
}
export default app;
My Component:
class Component extends React.PureComponent() {
render() {
return (
{
JSON.stringify(this.props.componentObject)
}
);
}
}
function mapStateToProps(state) {
return {
componentObject: state.myObject
};
}
export default connect(mapStateToProps)(Component);
Action creator for updating nested object
export function setData(newDeeplyNestedObject) {
return {
type: SET_DATA,
payload: newDeeplyNestedObject
};
}
The result is that my Component is not displaying what is in Redux's state. It appears that the Redux is not detecting a change in my components props thus not updating the Component. Can anyone tell me how to make sure Redux sends updates to my component when a deeply nested object structure changes?
I know that Redux does a shallow compare for updates, but I feel like a JSON.parse(JSON.stringify()) in my reducer is not the right direction here.
1) Implement a store by:
import {createStore} from "redux";
export const store = createStore(app) // you pass in your reducer name
2) In mapStateToProps, access your myObject state like:
function mapStateToProps(state){
return{
myObject:state.myObject
}
};
3) Bind your action dispatcher:
import {bindActionCreators} from "redux";
const mapDispatchToProps = dispatch =>(bindActionCreators({setData}, dispatch));
//Now we can use the setData action creator as this.props.setData()
4) Use both mapStateToProps and mapDispatchToProps and connect it to the component:
export default connect(mapStateToProps,mapDispatchToProps)(Component);
5) Suppose you defined myObjects in INITIAL_STATE, for example:
myObject : {
prop1 : {
prop2 : {
array1 : [{a:'1'},{b:'2'}],
array2: [{q:'6'},{w:'4'}],
array3: [{m:'9'},{n:'0'}]
}
}
}
};
6) Now you need to fire your action function setData using any changeHandlers or button press and pass the newObject which you wanna add as arguments. The idea is to spread the objects(using the ... operator) and append the new items which you wanna add. Suppose you wanna fire your action on an onClick event and change the array1 values:
<input type="button" onClick={()=>this.props.setData({...this.props.myObject.prop1.prop2,array1:[{changedA:'21'},{changedB:'32'}]})}/>
This will fire the action, and only the array1 will be changed. Similarly, you can do for any nested objects.
Whatever change you want to do, you have to spread your objects and then append the new items
PS: Also enclose the return in tags.

Redux change to nested object not triggering componentDidUpdate in component

I am struggling to figure out why a change to an object located in the store handled by a redux reducer is not triggering the componentDidUpdate method inside of my react component. I am using the react developer tools and can see the correct store after the state is reduced, and am also using redux logger and can see the correct after state after the reducer makes the change. But the component still never calls the update method.
action
export const GSAP_ANIMATION = 'GSAP_ANIMATION';
export const animateGsap = (key, next) => {
return {
type: GSAP_ANIMATION,
payload: {
key: key,
next: next
}
}
}
reducer
case GSAP_ANIMATION:
return Object.assign({}, state, {
...state,
gsap: {
...state.gsap,
[payload.key]: {
...state.gsap[payload.key],
next: {
...payload.next
}
}
}
});
component connection
const mapStateToProps = (state, ownProps) => {
return {
component: state.priorities.gsap[ownProps.id]
};
}
const mapDispatchToProps = (dispatch) => {
return {
addGsap: (key) => dispatch(actions.addGsap(key))
};
}
GsapComponent = connect(mapStateToProps, mapDispatchToProps)(GsapComponent);
In the GsapComponent I have the componentDidUpdate method, but this method is never called. However, I can see that the value of this.props.component should be correct when I view the component in the chrome extension.
edit
also doing { JSON.stringify(this.props.component) } correctly shows the updated prop values. Nothing in the react component update lifecycle is every triggered though.
I have also tried to use the immutibility-helper from react like so
return update(state, {
gsap: {
[payload.key]: {
$merge: { next: payload.next }
}
}
});
but it still doesn't call the lifecycle method.
GsapComponent source code.
Check this object assign documentation. Section Examples -> Warning for Deep Clone. I think that your reducer return object is === as state object so react can't detect change. Try json.parse(json.stringify) workaround or use immutable-js.

React component not re-rendering with connect()

My reducer:
export default function summary(state = {
"summary":null
}, action = null) {
switch (action.type) {
case GET_SUMMARY_REQUEST_SUCCESS:
const newState = Object.assign({}, state);
newState.summary = action.data;
return newState;
break;
case GET_SUMMARY_REQUEST_ERROR:
return Object.assign({}, state, {
sumary:null
});
break;
default: return state;
}
};
Root reducer:
import summary from './Summary.js'
const rootReducer = combineReducers({
summary
});
Inside my component, I am using connect to map state to props>
My components render function is something like this:
render() {
const summary = this.props.summaryContent || [];
return (
<div className={cx(styles['loading'])} >
<Loader width="4" />
{"Loading\u2026"}
</div>
);
}
function mapStateToProps(state, ownParams) {
return {
summaryContent: state.summary
};
}
export default connect(mapStateToProps)(Summary);
On componentWillMount, I am dispatching an action to update in the state in summary. Now, my componentWillReceiveProps is showing me the updated state in summary, but the component is not rendering.
I can see a few issues here:
You use summary both as a reducer key and as an index in your reducer state. That means summaryContent should map to state.summary.summary. (Though I'd suggest changing it to adopt a less confusing naming convention.)
Your render function doesn't utilise this.props.summaryContent in any useful way. It simply assigns it to a variable and then returns a loading output.
You've misspelled summary in the GET_SUMMARY_REQUEST_ERROR case.
I strongly recommend configuring ESLint which will alert you to issues like unused and misspelt variables.

Resources