Redux Action with For loop doesn't work as expected - reactjs

TLDR : Weird Mutations. Solved with re-assigning variables. But not the most efficient...
PREFACE
I have a component called Feed connected to redux
export class Feed extends Component {
bookmark = (id) => {
this.props.bookmarkItem(id).then(s=>{
this.props.updateFeed(this.props.feed, this.props.bookmarks);
}); //redux action with redux-thunk
}
render(){
//renders card items with a bookmark button on each card
//onPress button call this.bookmark(item.id)
}
}
const mapStateToProps = (state) => ({
feed: state.app.feed,
bookmarks: state.app.bookmarks
})
export default connect(mapStateToProps, { bookmarkItem, updateFeed })(Feed)
On pressing a button on the card, a this.bookmark() function is called and an action this.props.bookmarkItem(id) is called.
The bookmarkItem redux action looks like this.
export const bookmarkItem = (item_id) => async dispatch => {
try {
let r = await axios.post(`/bookmark`); //api call
let { bookmarks } = r; //get updated bookmarked items from api
dispatch({
type: cons.UPDATE_BOOKMARKED,
payload: { bookmarked }
});
return true;
}
catch (err) {
throw new Error(err);
}
}
Redux state looks like this
let app = {
feed: [{id:1}, {id:2}, {id:3}, {id:4}],
bookmarks:[{id:1}, {id:2}]
}
AIM
Now I gotta update the feed array in Redux State with property isBookmarked for that each item in feed.
(So that we can display that item is already bookmarked or not)
So for that I am calling another updateFeed() redux action
export const updateFeed = (feed, bookmarks) => dispatch => {
console.log('checkpoint 1', feed); //CHECKPOINT 1
for (let i = 0; i < feed.length; i++) {
if (bookmarks.length == 0) {
feed[i].isBookmarked = false; //mark item as NOT bookmarked
} else {
//check all bookmarks
for (let p = 0; p < bookmarks.length; p++) {
//if bookmark exists, set item in feed as isBookmarked:true
if (bookmarks[p].id === feed[i].id)
feed[i].isBookmarked = true; //mark item as bookmarked
}
}
}
dispatch({
type: cons.UPDATE_FEED,
payload: {
feed
}
});
};
PROBLEM
So there are a few problems I'm facing here:
At CHECKPOINT 1 the feed object logs a mutated feed with isBookmarked property in Google Chrome Console, even before the loop is started.
The payload that goes at UPDATE_FEED action is the with isBookmarked property, but the re-render of the feed never takes place.
HACKY WORKAROUND
Assigning feed to a new variable t in UPDATE_FEED action correctly mutates and then sends in the dispatch call.
But I'm not sure if this is the most efficient way to do it.
export const updateFeed = (feed, bookmarks) => dispatch => {
let t = JSON.parse(JSON.stringify(feed));
console.log('checkpoint 1', feed); //CHECKPOINT 1
for (let i = 0; i < t.length; i++) {
if (bookmarks.length == 0) {
t[i].isBookmarked = false; //mark item as NOT bookmarked
} else {
//check all bookmarks
for (let p = 0; p < bookmarks.length; p++) {
//if bookmark exists, set item in feed as isBookmarked:true
if (bookmarks[p].id === t[i].id)
t[i].isBookmarked = true; //mark item as isBookmarked
}
}
}
dispatch({
type: cons.UPDATE_FEED,
payload: {
feed : t //THIS WORKS CORRECTLY
}
});
};
Is waiting until that for loop is finished asynchronously a better way to do it ?
EXTRAS
Reducer looks like this. Pretty simple, only updates with new values, all cases handled by single switch method.
const initialState = {
feed: [],
bookmarks:[]
}
const app = (state = initialState, action) => {
console.log('appReducer', action, JSON.stringify(state));
switch (action.type) {
case cons.UPDATE_BOOKMARKS:
case cons.UPDATE_FEED:
{
state = {
...state,
...action.payload,
}
break;
}
}
return state;
}
export default app;

Assigning feed to a new variable t in UPDATE_FEED action correctly mutates
It simply means,your reducers not creating new state object.Its just mutate the state in place.
It uses === to check state update in state tree.
...action.payload it coping the same reference for feed object and nested element as it is in the new state.So,there is no state update.
Change cons.UPDATE_FEED like this.
state = {
...state,
...action.payload.feed,
}

Related

React Redux await until first action is complete

I have an action that will take quite some time to complete. I want to inform the users that the action is currently taking place. To do this I am dispatching an action that updates a reducers state with a boolean.
The issue I am running into is that the larger action starts before the the state is updated. As a result, when a users clicks the generate button, they then experience the system lagging appose to UI informing them to wait.
How can I ensure the first action startGeneration() is completed before buildKeyMap() starts? Same questioned for reportProgress(). I have redux-thunk installed.
export const generateKeyMap = (mapFrom, mapTo) => {
return async (dispatch) => {
dispatch(startGeneration()), // <-- this updates reducers state with a boolean, generatingMap: true
dispatch(buildKeyMap(mapFrom, mapTo)) // <-- this takes a long time to finish
};
};
export const buildKeyMap = (mapFrom, mapTo) => {
return async (dispatch) => {
let keyMap = {};
for (let i = 0; i < mapFrom.length; i++) {
dispatch(reportProgress(i)); // <-- would like this to finish before moving on in the loop
let randomIndex = randomIntFromInterval(0, mapTo.length - 1);
let key = mapFrom[i].toString();
let value = mapTo[randomIndex].toString();
Object.assign(keyMap, { [key]: value });
mapTo.splice(randomIndex, 1);
}
dispatch(stopGeneration()); // <--- Would like this to finish before delivering the map
return { type: actionTypes.DELIVERKEYMAP, payload: keyMap };
};
};
export const startGeneration = () => {
return { type: actionTypes.STARTKEYMAPGENERATION };
};

Filter function is not working in react/redux

I am newbie in react/redux and I try to do an app to manage my sentences.
I try to filter my sentences and it does it correct but when i delete some words from the input field, my state remains filtered.
this is the action.
export const filterWord = (e) => {
return {
type: 'FILTER_WORD',
e
}
}
this is the reducer:
case 'FILTER_WORD':
return state.filter((sentence) => sentence.word.includes(action.e.target.value));
Hope someone can give me an advice
The best way to handling filtering is not filter your reducer state based on the filter word but to do that in a selector
If you filter your state in a reducer, the next time you change a filter or remove it, you would not have the original state but a duplicate state
You can instead store the filterword in reducer
export const filterWord = (e) => {
return {
type: 'FILTER_WORD',
word: e.target.value,
}
}
const reducer = (state = { data: [], filterWord: null}) => {
...
case 'FILTER_WORD':
return {...state, filterWord: action.word};
...
}
Now in your mapStateToProps component you can filter the result for the user component
const mapStateToProps = (state) => {
return {
data: state.data.filter((sentence) => sentence.word.includes(state.filterWord));
}
}

Redux , state.concat is not a function at rootReducer. And being forced to reRender an element for it to see the state change

So I have this sidebar component where I load my store and my dispatcher
//select
const mapStateToProps = state => {
return { renderedEl: state.renderedEl }
}
function mapDispatchToProps(dispatch) {
return{
renderLayoutElement: element => dispatch(renderLayoutElement(element))
}
}
Then inside the same component this Is how I trigger the dispatcher
renderEl = (el) => {
var elementName = el.target.getAttribute('id');
var renderedElements = this.props.renderedEl; //this is data from the store
for (let key in renderedElements) {
if (key == elementName) {
renderedElements[key] = true
}
}
this.props.renderLayoutElement({renderedElements});
}
Then as I understand it gets sent to the reducer
import {RENDER_LAYOUT_ELEMENT} from "../constants/action-types"
const initialState = {
renderedEl: {
heimdall: false,
skadi: false,
mercator: false
}
}
function rootReducer(state = initialState, action){
if(action.type === RENDER_LAYOUT_ELEMENT){
return Object.assign({},state,{
renderedEl: state.renderedEl.concat(action.payload)
})
}
return state
}
export default rootReducer;
This is its action
import {RENDER_LAYOUT_ELEMENT} from "../constants/action-types"
export function renderLayoutElement(payload) {
return { type: RENDER_LAYOUT_ELEMENT, payload }
};
Now the thing is. Im receiving a
state.renderedEl.concat is not a function at rootreducer / at dispatch
I dont understand why does that happen.
Becuase, actually the store gets updated as I can see, but the console returns that error. And I have to reload the render that uses the props of that store (with an onhover) in order to be able to see the changes. It doesnt happen automatically as it would happen with a state
if(action.type === RENDER_LAYOUT_ELEMENT){
return { ...state, renderedEl: { ...state.renderedEl, ...action.payload } };
}
Duplicate from comments maybe it can be helpful to someone else :)

Object passed into Redux store is not reflecting all key/values after mapStateToProps

I have a component where toggle buttons are dynamically generated. Right now, I am just trying to get it working at a basic level so you click on a button and it adds a key/value pair to the cuts = {}.
After clicking on multiple buttons the cuts should have several key/value pairs: it does in the component where cuts resides, it does in the action, and it does in the Redux store via console.log(state.cuts).
However, after mapStateToProps it is only showing the first value and I am not sure why.
Anyway, here is my code and the flow as it is initiated by the user:
// bq_cuts.js component
constructor(props) {
super(props);
this.state = {
cuts: {}
}
}
onCutSelect(cut) {
const { bqResults } = this.props;
const { cuts } = this.state;
let key = cut.name;
let value = cut.value;
cuts[key] = value;
this.setState({
cuts
})
console.log(cuts); // shows all of the selected cuts here
bqResults(cuts);
}
// results.js actions
export function bqResults(results) {
console.log(results); // shows all of the selected cuts here
return function(dispatch) {
dispatch({
type: FILTER_RESULTS,
payload: results
})
}
}
// results.js reducer
import {
FILTER_RESULTS
} from '../actions/results';
export default function(state = {}, action) {
switch(action.type) {
case FILTER_RESULTS:
console.log(action.payload); //prints out all the cuts
return {
...state,
filter_results: action.payload
}
default:
return state;
}
return state;
}
const rootReducer = combineReducers({
results: resultsReducer,
});
export default rootReducer;
// bq_results.js component where the FILTER_RESULTS is accessed
render() {
console.log(this.props.filter_results); // only shows the first result
return (<div>...</div>)
}
function mapStateToProps(state) {
console.log(state.results.filter_results); // shows all selected cuts here
return {
filter_results: state.results.filter_results,
}
}
Maybe a better way of putting it is it seems like after the initial state is mapped to props, it is no longer receiving changes to state and mapping it to props.
Came across this article and used Approach #2:
https://medium.freecodecamp.org/handling-state-in-react-four-immutable-approaches-to-consider-d1f5c00249d5
Ended up with:
onCutSelect(cut) {
let cuts = {...this.state.cuts, [cut]: cut}
this.setState({
cuts
}, () => this.props.bqResults(this.state.cuts));
}

React/Redux and API data object

My app successfully gets API data and puts it to Redux state tree.
{
"coord":{
"lon":-0.13,
"lat":51.51
},
"weather":[
{
"id":311,
"main":"Drizzle",
"description":"drizzle rain",
"icon":"09d"
},
{
"id":501,
"main":"Rain",
"description":"moderate rain",
"icon":"10d"
}
],
//--------
//--------
"id":2643741,
"name":"London",
"cod":200
}
Props.data has been passed to components but in reality I have an access only to the
first key. For example props.data.name, props.data.id are accesiible. But props.data.coord.lon
and props.data.weather.map(---), are undefined.
Please, what's wrong with my understanding of using API dataset?
Component
export const DayItem = (props) => {
return (<MuiThemeProvider>
<Paper zDepth={2}>
{props.data.coord.lon} // No way!
{props.data.name} // OK!
</Paper>
</MuiThemeProvider>)}
Saga that gets data and dispatches an action. Puts data to Redux store.
function* getPosition() {
const getCurrentPosition = () => new Promise(
(res, rej) => navigator.geolocation.getCurrentPosition(res, rej))
// Gets user's current position assigned to const
const pos = yield call(getCurrentPosition);
const {latitude, longitude} = pos.coords;
// Yields the forecast API by user coordinates
const data = yield call(getForecastByCoords, latitude, longitude)
// Yields user's local forecast to the reducer
yield put({
type: LOAD_DATA_SUCCESS,
data
});
}
And mapStateToProps
function mapStateToProps(state) {
return {
chips: state.chipsReducer,
data: state.dataReducer
}
};
dataReducer
export const dataReducer = (state = {}, action) => {
switch (action.type) {
case LOAD_DATA_SUCCESS:
return action.data;
default:
return state;
}
};
Eventually, I got the point.
The problem was in the difference of speed React rendering vs Data loading.
Loading is always behind rendering. So, the complete set of data had no existence.
Just conditional rendering made my day {this.state.isLoading ? <div>Loading</div> : <DayItem {...this.props}/>}
you must use MapStateToProps, then use componentWillReceiveProps(nextProps)
Something like this
function mapStateToProps(state) {
return {
data:state.toJS().yourReducer.data,
}
then do next:
componentWillReceiveProps(nextProps) {
if (nextProps.data) {
if (nextProps.data.coord.lon != undefined) {
this.setState({yourData:nextProps.data.coord.lon}
}
}

Resources