React Redux - update child in state array - reactjs

So I have an object in Redux store called currentAccount. It has a child array called groups, which has a child array called tasks.
Now, I could update one task real easily in Angular by more-or-less going:
function updateTask(task){
http.post('xyz/updateTask', function(updatedTask){
task = updatedTask;
});
}
...and that'd work just fine.
In React, I can dispatch the data to actions, post it to the API with Axios, but then... how do I find and update the old task?
I could:
Get the server to return the entire currentAccount object again, then turn it into a JSON string and back (forcing Redux to re-render the whole tree),
...or do a forEach within a forEach to find the task by its ID, then replace it (although I'm not sure Redux would pick up on the change)
But both of these strike me as totally insane. Is there a simpler way, or is that just sort of how React works?
Apologies if this is a dumb question, I'm not really sure how else to word it, haha.

Once you have your response from your http call, dispatch an action that updates the store. You would need access to dispatch in your component, so you would use mapDispatchToProps to provide the functionality.
Here is a great starter tutorial for Redux by the creator, Dan Abramov.
This code example should help you.
// I'm using this stateless functional component to
// render the presentational view
const Task = ({ label, info }) => (
<div className="task">
<h3{ label }</h3>
<p{ info }</p>
</div>
);
// this is the class that receives props from connect
// and where we render our tasks from
class Tasks extends PureComponent {
// once your component is mounted, get your
// http data and then disptach update action
componentDidMount() {
// using axios here but you can use any http library
axios.post( "/some_url", task )
.then( ( response ) => {
// get update provided by mapDispatchToProps
// and use it to update tasks with response.data
const { update } = this.props;
update( response.data );
})
.catch( ( error ) ) => {
// handle errors
}
}
render() {
// destructure tasks from props
const { tasks } = this.props;
// render tasks from tasks and pass along props
// if there are no tasks in the store, return a loading indicator
return (
<div className="tasks">
{
tasks ? tasks.map(( task ) => {
return <Task
label={ task.label }
info={ task.info }
/>
}) :
<div className="loading">Loading...</div>
}
</div>
);
}
}
// this will provide the Tasks component with props.task
const mapStateToProps = ( state ) => {
return {
tasks: state.tasks
}
}
// this will provide the Tasks component with props.update
const mapDispatchToProps = ( dispatch ) => {
return {
update: ( tasks ) => {
dispatch({
type: "UPDATE_TASKS",
tasks
});
}
}
}
// this connects Task to the store giving it props according to your
// mapStateToProps and mapDispatchToProps functions
export default Task = connect( mapStateToProps, mapDispatchToProps )( Task );
You will need a reducer that handles the "UPDATE_TASK" action. The reducer updates the store, and considering the component is connected, it will receive new props with the updated store value and the tasks will updated in the DOM.
EDIT: To address the reducers, here is an additional example.
import { combineReducers } from "redux";
const tasks = ( state = [], action ) => {
switch( action.type ) {
case "UPDATE_TASKS":
// this will make state.tasks = action.tasks which
// you dispatched from your .then method of the http call
return action.tasks;
default:
return state;
}
};
const other = ( state = {}, action ) => {
...
}
const combinedReducers = combineReducers({
tasks,
other
});
const store = createStore(
combinedReducers/*,
persisted state,
enhancers*/
);
/*
The above setup will produce an initial state tree as follows:
{
tasks: [ ],
other: { }
}
After your http call, when you dispatch the action with the tasks in
the response, your state would look like
{
tasks: [ ...tasks you updated ],
other: { }
}
*/

If anyone else gets stuck on this I think I've sorted it out.
I just pass along all the indexes of the task's parents, then use 'dotPoints' to set the state like this:
dotProp.set(state, `currentProject.groups.${action.groupIndex}.tasks.${action.taskIndex}`, action.task);
Seems like a fairly neat solution so far.

Related

Update single element of the array in redux state

I have a redux state that contains an array of objects, for each of these object I call an api to get more data
objects.forEach((obj, index) => {
let newObj = { ...obj };
service.getMoreData()
.then(result => {
newObj.data = result;
let newObjects = [...this.props.objectsList] ;
let index = newObjects.findIndex(el => el.id === newObj.id);
if (index != -1) {
newObjects[index] = newObj;
this.props.updateMyState({ objectsList: newObjects });
}
})
When I get two very close responses the state is not updated correctly, I lose the data of the first response.
What is the right way to update a single element of the array? Thanks!
So since i don't know what service is and there isn't that much here to go off, here is what I would do from my understanding of what it looks like your doing:
So first let's set up a reducer to handle the part of redux state that you want to modify:
// going to give the reducer a default state
// array just because I don't know
// the full use case
// you have an id in your example so this is the best I can do :(
const defaultState = [{ id: 123456 }, { id: 123457 }];
const someReducer = (state=defaultState, action) => {
switch (action.type) {
// this is the main thing we're gonna use
case 'UPDATE_REDUX_ARRAY':
return [
...action.data
]
// return a default state == the state argument
default:
return [
...state
]
}
}
export default someReducer;
Next you should set up some actions for the reducer, this is optional and you can do it all inline in your component but I'd personally do it this way:
// pass data to the reducer using an action
const updateReduxArray = data => {
return {
type: 'UPDATE_REDUX_ARRAY',
data: data
}
}
// export like this because there might
// be more actions to add later
export {
updateReduxArray
}
Then use the reducer and action with React to update / render or whatever else you want
import { useState } from 'react';
import { updateReduxArray } from 'path_to_actions_file';
import { useEffect } from 'react';
import { axios } from 'axios';
import { useSelector, useDispatch } from 'react-redux';
const SomeComponent = () => {
// set up redux dispatch
const dispatch = useDispatch();
// get your redux state
const reduxArray = useSelector(state => state.reduxArray) // your gonna have to name this however your's is named
// somewhere to store your objects (state)
const [arrayOfObjects, updateArrayOfObjects] = useState([]);
// function to get data from your API
const getData = async () => {
// I'm using axios for HTTP requests as its pretty
// easy to use
// if you use map you can just return the value of all API calls at once
const updatedData = await Promise.all(reduxArray.map(async (object, index) => {
// make the api call
const response = axios.get(`https://some_api_endpoint/${object.id}`)
.then(r => r.data)
// return the original object with the addition of the new data
return {
...response,
...object
}
}))
// once all API calls are done update the state
// you could just update redux here but this is
// a clean way of doing it incase you wanna update
// the redux state more than once
// costs more memory to do this though
updateArrayOfObjects(updatedData)
}
// basicity the same as component did mount
// if you're using classes
useEffect(() => {
// get some data from the api
getData()
}, [ ])
// every time arrayOfObjects is updated
// also update redux
useEffect(() => {
// dispatch your action to the reducer
dispatch(updateReduxArray(arrayOfObjects))
}, [arrayOfObjects])
// render something to the page??
return (
<div>
{ reduxArray.length > 0
? reduxArray.map(object => <p>I am { object.id }</p>)
: <p>nothing to see here</p>
}
</div>
)
}
export default SomeComponent;
You could also do this so that you only update one object in redux at a time but even then you'd still be better off just passing the whole array to redux so I'd do the math on the component side rather than the reducer .
Note that in the component I used react state and useEffect. You might not need to do this, you could just handle it all in one place when the component mounts but we're using React so I just showcased it incase you want to use it somewhere else :)
Also lastly I'm using react-redux here so if you don't have that set up (you should do) please go away and do that first, adding your Provider to the root component. There are plenty of guides on this.

Wait for redux action to finish dispatching when using redux saga

I have a redux saga setup which works fine. One of my dispatches is to create a new order, then once that has been created I want to do things with the updated state.
// this.props.userOrders = []
dispatch(actions.createOrder(object))
doSomethingWith(this.props.userOrders)
Since the createOrder action triggers a redux saga which calls an API, there is a delay, so this.props.userOrders is not updated before my function doSomethingWith is called. I could set a timeout, but that doesn't seem like a sustainable idea.
I have read the similar questions on Stack Overflow, and have tried implementing the methods where relevant, but I can't seem to get it working. I'm hoping with my code below that someone can just add a couple of lines which will do it.
Here are the relevant other files:
actions.js
export const createUserOrder = (data) => ({
type: 'CREATE_USER_ORDER',
data
})
Sagas.js
function * createUserOrder () {
yield takeEvery('CREATE_USER_ORDER', callCreateUserOrder)
}
export function * callCreateUserOrder (newUserOrderAction) {
try {
const data = newUserOrderAction.data
const newUserOrder = yield call(api.createUserOrder, data)
yield put({type: 'CREATE_USER_ORDER_SUCCEEDED', newUserOrder: newUserOrder})
} catch (error) {
yield put({type: 'CREATE_USER_ORDER_FAILED', error})
}
}
Api.js
export const createUserOrder = (data) => new Promise((resolve, reject) => {
api.post('/userOrders/', data, {headers: {'Content-Type': 'application/json'}})
.then((response) => {
if (!response.ok) {
reject(response)
} else {
resolve(data)
}
})
})
orders reducer:
case 'CREATE_USER_ORDER_SUCCEEDED':
if (action.newUserOrder) {
let newArray = state.slice()
newArray.push(action.newUserOrder)
return newArray
} else {
return state
}
This feels like an XY Problem. You shouldn't be "waiting" inside a component's lifecycle function / event handler at any point, but rather make use of the current state of the store.
If I understand correctly, this is your current flow:
You dispatch an action CREATE_USER_ORDER in your React component. This action is consumed by your callCreateUserOrder saga. When your create order saga is complete, it dispatches another "completed" action, which you already have as CREATE_USER_ORDER_SUCCEEDED.
What you should now add is the proper reducer/selector to handle your CREATE_USER_ORDER_SUCCEEDED:
This CREATE_USER_ORDER_SUCCEEDED action should be handled by your reducer to create a new state where some "orders" property in your state is populated. This can be connected directly to your component via a selector, at which point your component will be re-rendered and this.props.userOrders is populated.
Example:
component
class OrderList extends React.PureComponent {
static propTypes = {
userOrders: PropTypes.array.isRequired,
createOrder: PropTypes.func.isRequired,
}
addOrder() {
this.props.createOrder({...})
}
render() {
return (
<Wrapper>
<Button onClick={this.addOrder}>Add Order</Button>
<List>{this.props.userOrders.map(order => <Item>{order.name}</Item>)}</List>
</Wrapper>
)
}
}
const mapStateToProps = state => ({
userOrders: state.get('userOrders'),
})
const mapDispatchToProps = {
createOrder: () => ({ type: 'CREATE_ORDER', payload: {} }),
}
export default connect(mapStateToProps, mapDispatchToProps)(OrderList)
reducer
case 'CREATE_USER_ORDER_SUCCEEDED':
return state.update('userOrders',
orders => orders.concat([payload.newUserOrder])
)
If you really do need side-effects, then add those side-effects to your saga, or create a new saga that takes the SUCCESS action.

Subscribing to a redux action in a react component

I have an async thunk that fetches some information from a web service, it can dispatch three types of actions
FETCH_REQUESTED
FETCH_SUCCEEDED
FETCH_FAILED
Finally, if it's succeeded; it returns the actual response, or an error object.
I have a component that should detect whether the operation has failed or not, preferably by subscribing to the FETCH_FAILED action and displaying an error message based on the type of the error (404/401 and other status codes)
export const fetchData = () => {
return async (dispatch, getState) => {
const appState = getState();
const { uid } = appState.appReducer;
await dispatch(fetchRequested());
try {
const response = await LookupApiFactory().fetch({ uid });
dispatch(fetchSucceeded(response));
return response;
} catch (error) {
dispatch(fetchFailed());
return error;
}
}
}
I'm quite new to redux and react, so I'm a bit unsure if I'm heading in the right direction, any help would be appreciated.
To implement a proper redux call back and storage mechanism you should have a store to keep all your data,
const store = createStore(todos, ['Use Redux'])
then, you dispatch data to store,
store.dispatch({
type: 'FETCH_FAILED',
text: reposnse.status //Here you should give the failed response from api
});
Then you can get the value from the store in any of your components using a subscribe function. It will be called any time an action is dispatched, and some part of the state tree may potentially have changed.
store.subscribe(()=>{
store.getState().some.deep.property
})
This is a simple implementation of Redux. As your app grows more complex, you'll want to split your reducing function into separate functions, each managing independent parts of the state using combineReducers. You can get more information from redux.js site
The most common approach is to use connect function from react-redux library. This is a HoC which subscribes to state changes. Take a look at this library, additionally it allows you to bind your action creators to dispatch, what gives you an ability to dispatch your actions from component.
You can use it like this:
import React from 'react';
import { connect } from 'react-redux';
const MyComponent = ({ data, error }) => (
<div>
{error && (
<span>Error occured: {error}</span>
)}
{!error && (
<pre>{JSON.stringify(data, null, 2)}</pre>
)}
</div>
);
const mapStateToProps = (state) => ({
data: state.appReducer.data,
error: state.appReducer.error
});
export default connect(mapStateToProps)(MyComponent);
You can use conditional rendering inside your jsx as I've shown above, or use guard clause, like this:
const MyComponent = ({ data, error }) => {
if (error) {
return (
<span>Error occured: {error}</span>
);
}
return (
<pre>
{JSON.stringify(data, null, 2)}
</pre>
);
}
Assuming reducers,
for FETCH_FAILED action,you can put some meaningful flag indicating
there are some failure.Based on that flag you can show error messages or do other action.
const testReducers =(state,actione)=>{
case 'FETCH_FAILED' : {
return {
...state,{ error_in_response : true }
}
};
default : return state;
}
In your container,you can get that flag and passed it to your component.
Assuming combineReducers used to combine reducers;
const mapStateToProps=(state)=>{
return {
error_in_response : state.testReducers.error_in_response
}
}
connect(mapStateToProps)(yourComponent)
In your component, this can be accessed using this.props.error_in_response

Is there a better way to handle this redux recompose branch use case?

I have a component called PartyDetails, which needs data fetched by an ajax call. I want to show a Loading component while the ajax request is in progress.
The problem is that in order to determine whether the data is loaded or not, I need access to the store. This is how my enhance looks like:
const enhance = compose(
propSetter,
lifecycleEnhancer,
loading,
)
export default enhance(PartyDetails)
where propSetter is:
const propSetter = connect((state) => {
const { party } = state
const { dataLoaded } = party
// for some reason state does not contain match, and I'm resorting to routing
const { routing: {location: { pathname }}} = state
const involvedPartyId = pathname.substring(pathname.lastIndexOf('/') + 1)
return { dataLoaded, involvedPartyId }
}, {loadParty})
and lifecycleEnhancer is:
const lifecycleEnhancer = lifecycle({
componentDidMount() {
this.props.loadParty(this.props.involvedPartyId)
}
})
and loading is ( notice that in this case, dataLoaded comes from the previous connect that has been done in propSetter ):
const loading = branch(
({dataLoaded}) => dataLoaded,
renderComponent(connect(mapStateToProps, mapDispatchToProps)(PartyDetails)),
renderComponent(Loading)
)
So basically, if the data has been fetched, I am using a 2nd connect to obtain the relevant props for PartyDetails.
I just started learning recompose a few days ago, and I could not find an example that fitted my use case. The above is what I came up with after reading through the docs, and some examples found in other articles.
Is what I'm doing a good way of handling this? Could this be done in a better way, maybe without needing 2 connect calls?
You could write all your logic for mapping state and dispatch to props in one connect:
export default compose(
connect(
state => {
const { party } = state;
const { dataLoaded } = party;
const { routing: { location: { pathname } } } = state;
const involvedPartyId = pathname.substring(pathname.lastIndexOf("/") + 1);
// also put here your `mapStateToProps` code
return { dataLoaded, involvedPartyId };
},
{
loadParty
// the same here, append `mapDispatchToProps` logic
}
),
lifecycle({
componentDidMount() {
this.props.loadParty(this.props.involvedPartyId);
}
}),
branch(
({ dataLoaded }) => dataLoaded,
renderComponent(PartyDetails),
renderComponent(Loading)
)
// as `branch` is returning HOC, we need to provide empty component to it in
// order to render whole component
)(createSink());

Correct way to pre-load component data in react+redux

I do not know the correct way to pre-load data from API for a component to use.
I have written a stateless component which should render the data:
import React, { PropTypes } from 'react';
const DepartmentsList = ({ departments }) => {
const listItems = departments.map((department) => (
<li>{department.title}</li>
));
return (
<ul>
{listItems}
</ul>
);
};
DepartmentsList.propTypes = {
departments: PropTypes.array.isRequired
};
export default DepartmentsList;
And I have an action which will retreive data from the API:
import { getDepartments } from '../api/timetable';
export const REQUEST_DEPARTMENTS = 'REQUEST_DEPARTMENTS';
export const RECEIVE_DEPARTMENTS = 'RECEIVE_DEPARTMENTS';
const requestDepartments = () => ({ type: REQUEST_DEPARTMENTS });
const receiveDepartments = (departments) => ({ type: RECEIVE_DEPARTMENTS, departments });
export function fetchDepartments() {
return dispatch => {
dispatch(requestDepartments);
getDepartments()
.then(departments => dispatch(
receiveDepartments(departments)
))
.catch(console.log);
};
}
Now I think I have a few options to preload departments that are required for the list. I could use redux-thunk and mapDispatchToProps to inject fetchDepartments to the stateless component and implement componentWillMount or similar lifecycle method, to load data - but then I don't need to pass the list via props, as the component would always load data for himself, and I don't want that, because whenever a new component is created the data is fetched from api instead of store...
Another advice I've seen is to use getComponent function from react-router, and retreive all data before returning the component, however, I am not sure if it's the correct redux way, as I don't see how to use redux-thunk there, and logic kind of seems littered all accross the files, when it's the data required for only one component.
This leaves me with the only seemingly ok option to load data in container component's lifecycle methods, but I want to know what is considered the best practice for what I want to do.
The most 'redux-like' way of handling the pre-loading of data would be to fire off the asynchronous action in the lifecycle method (probably componentWillMount) of a Higher Order Component that wraps your app. However, you will not use the results of the API call directly in that component - it needs to be handled with a reducer that puts it into your app store. This will require you to use some sort of a thunk middleware to handle the asynchronous action. Then you will use mapStateToProps to simply pass it down to the component that renders the data.
Higher Order Component:
const mapStateToProps = (state) => {
return {
departments: state.departments
};
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
getDepartments: actionCreators.fetchDepartments
});
}
class App extends Component {
componentWillMount() {
this.props.getDepartments();
}
render() {
return <DepartmentsList departments={this.props.departments} />
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
reducers:
export function departments(state = [], action) {
switch(action.type) {
case 'RECEIVE_DEPARTMENTS':
return action.departments;
}
}

Resources