Handle request call responses in react-redux app to avoid using callbacks - reactjs

This is rather a design question, but I'm wondering if there's a best practice to handle such situations.
I have a react redux app. The app state structure looks like this:
{
groups: {gid1: group1, gid2: group2, ....},
expenses: {eid1: expense1, eid2: expense2, ...},
//.... the rest
}
The NewGroup component is aimed to create a new group. If the create request (ajax request to the backend) is successful , it is added to the list of groups in the app state. The NewGroup, which is a modal, is closed and list of groups updates.
Though the NewGroup component should not be closed if the request was not successful.
The main question is: Should I incorporate the request status (if it is successful or unsuccessful, the error message in the latter case, if the result is returned or not, etc.) in the app state ? This is where I am really doubtful because it makes the app state very ugly, large and in some cases it is incapable of handling some situations, in this case: Creating a new group.
Let's say I add the response status code and error message to the app state:
{
groups: {
newGroupId1: {
data: newGroupData1,// null in case it was not successful
isLoading: true/false, // if the call is returned or not
errorFetching: message1
}
}
}
On the second unsuccessful create request to the backend, it is not possible to distinguish between the previous and the current calls, as both of them have errorFetching and similar isLoading values. (for example both have "No group found" for errorFetching and "false" for isLoading)
What is the best practice to distinguish between the status of the request calls and the real data in the app state when redux is being used ?
Update 1
This is the reducer:
import { FETCH_GROUPS_SUCCESS, FETCH_GROUPS_ERROR, CREATE_GROUP_SUCCESS, CREATE_GROUP_ERROR, DELETE_GROUP, FETCH_GROUP_SUCCESS, FETCH_GROUP_ERROR } from '../actions/creators';
export const FULL_GROUP = 'full_group';
export const SNIPPET_GROUP = 'snippet_group';
const INITIAL_STATE = {
data: null,
isLoading: true,
errorFetching: null
};
export default function(state=INITIAL_STATE, action) {
switch(action.type) {
case FETCH_GROUPS_SUCCESS:
return {
data: _.zipObject(_.map(action.payload, group => group.id),
_.map(action.payload, group => ({
data: group,
isLoading: true,
errorFetching: null,
mode: SNIPPET_GROUP
}))
),
isLoading: false,
errorFetching: null
};
case FETCH_GROUPS_ERROR:
return {
data: null,
isLoading: false,
errorFetching: action.payload.error
};
case FETCH_GROUP_SUCCESS:
return {
data: {...state.data, [action.payload.id]: {
data: action.payload,
isLoading: false,
errorFetching: null,
mode: FULL_GROUP
}},
isLoading: false,
errorFetching: null
};
case FETCH_GROUP_ERROR:
const stateWithoutId = _.omit(state, action.payload.id);
return {
data: {...stateWithoutId},
isLoading: false,
errorFetching: null
};
case CREATE_GROUP_SUCCESS:
debugger;
//TODO: how to let the NewGroup know that a group was created successfuly ?
return { ...state, [action.payload.id]: {
data: action.payload,
isLoading: true,
errorFetching: null,
mode: SNIPPET_GROUP
}};
default:
return state;
}
}
This is the action creator:
export function groupsFetchSucceeded(response) {
return {
type: FETCH_GROUPS_SUCCESS,
payload: response
}
}
export function groupsFetchErrored(errorWithId) {
return {
type: FETCH_GROUPS_ERROR,
payload: errorWithId
}
}
export function groupCreateSucceeded(group) {
return {
type: CREATE_GROUP_SUCCESS,
payload: group
}
}
export function groupCreateErrored(errorWithId) {
return {
type: CREATE_GROUP_ERROR,
payload: response
}
}
export function groupFetchSucceeded(groupData) {
return {
type: FETCH_GROUP_SUCCESS,
payload: groupData
}
}
export function groupFetchErrored(errorWithId) {
//handle the error here
return {
type: FETCH_GROUP_ERROR,
payload: errorWithId
}
}
This is the component. PLEASE! disregard the fact that this is a redux form, I don't care it's a redux form or not, I'm looking for a general idea. You can assume that it is a react-redux component with mapStateToProps and mapDispatchToProps and other related stuff.
import { createGroup } from '../../actions';
import { validateName, validateDescription } from '../../helpers/group_utils';
import { renderField, validate } from '../../helpers/form_utils';
const validators = {
name: validateName,
description: validateDescription
};
class NewGroup extends Component {
_onSubmit(values) {
this.props.createGroup(values);
}
render() {
const { handleSubmit } = this.props;
return (
<div>
<form onSubmit={handleSubmit(this._onSubmit.bind(this))}>
<Field name="name" label="Name" type="text" fieldType="input" component={renderField.bind(this)}/>
<Field name="description" label="Description" type="text" fieldType="input" component={renderField.bind(this)}/>
<button type="submit" className="btn btn-primary">Create</button>
<button type="button" className="btn btn-primary" onClick={() => this.props.history.push('/group')}>Cancel</button>
</form>
</div>
);
}
}
export default reduxForm({
validate,
//a unique id for this form
form:'NewGroup',
validators
})(
connect(null, { createGroup })(NewGroup)
);

The tricky part of your problem is that while you want to record all request statuses in redux's state, the number of requests might increase exponentially and take a lot of state's volume if not being handled well. On the other hand, I don't think distinguishing request data and state data is a big deal: you only need to separate them in state:
{
groups: {gid1: group1, gid2: group2, ....},
groupRequests: {grid1: request1, grid2: request2, ...},
expenses: {eid1: expense1, eid2: expense2, ...},
...
}
where each group data contains:
{
data: groupData,
lastRequestId: grid // optional
}
and each group request data contains:
{
groupId: gid,
isLoading: true/false,
errorFetching: message
}
When new request is sent, you update in groupRequests and track it there; NewGroup component only need to hold groupId to find the latest request fired and look after its status. And when you need to know the status of previous requests, take them from the list.
Again, the problem with this approach is that most of the data in groupRequests is one-time-used, meaning at some point (e.g. when NewGroup component closes), you don't even need it anymore. So make sure that you properly throw old request status away before groupRequest grows too big.
My advice is that when dealing with redux state, try to normalize it properly. If you need an 1-n relationship (group data and request data of creating group), break it down further instead of grouping them together. And avoid storing one-time-used data as much as possible.

Related

Redux reducer: Add object to array when the array doesn't have the object data

I'm trying to store AJAX call returned data object to an array of reducer state of Redux.
I have some conditions to check if the fetched data already exists inside of the reducer.
Here are my problems:
The component to call AJAX call actions, it's a function from mapDispatchToProps, causes an infinite loop.
isProductLikedData state in reducer doesn't get updated properly.
Can you tell what I'm missing?
Here are my code:
isProductLikedActions.js - action to fetch isProductLiked data. response.data looks like {status: 200, isProductLiked: boolean}
export function fetchIsProductLiked(productId) {
return function (dispatch) {
axios
.get(`/ajax/app/is_product_liked/${productId}`)
.then((response) => {
dispatch({
type: 'FETCH_IS_PRODUCT_LIKED_SUCCESS',
payload: { ...response.data, productId },
});
})
.catch((err) => {
dispatch({
type: 'FETCH_IS_PRODUCT_LIKED_REJECTED',
payload: err,
});
});
};
}
isProductLikedReducer.js - I add action.payload object to isProductLikedData array when array.length === 0. After that, I want to check if action.payload object exists in isProductLikedData or not to prevent the duplication. If there is not duplication, I want to do like [...state.isProductLikedData, action.payload].
const initialState = {
isProductLikedData: [],
fetching: false,
fetched: false,
error: null,
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case 'FETCH_IS_PRODUCT_LIKED': {
return { ...state, fetching: true };
}
case 'FETCH_IS_PRODUCT_LIKED_SUCCESS': {
return {
...state,
fetching: false,
fetched: true,
isProductLikedData:
state.isProductLikedData.length === 0
? [action.payload]
: state.isProductLikedData.map((data) => {
if (data.productId === action.payload.productId) return;
if (data.productId !== action.payload.productId)
return action.payload ;
}),
};
}
case 'FETCH_IS_PRODUCT_LIKED_REJECTED': {
return {
...state,
fetching: false,
error: action.payload,
};
}
}
return state;
}
Products.js - products is an array that fetched in componentWillMount. Once nextProps.products.fetched becomes true, I want to call fetchIsProductLiked to get isProductLiked` data. But this causes an infinite loop.
class Products extends React.Component {
...
componentWillMount() {
this.props.fetchProducts();
}
...
componentWillReceiveProps(nextProps) {
if (nextProps.products.fetched) {
nextProps.products.map((product) => {
this.props.fetchIsProductLiked(product.id);
}
}
render() {
...
}
}
export default Products;
Issue 1
The component to call AJAX call actions, it's a function from mapDispatchToProps, causes an infinite loop.
You are seeing infinite calls because of the condition you used in componentWillReceiveProps.
nextProps.products.fetched is always true after products (data) have been fetched. Also, note that componentWillReceiveProps will be called every time there is change in props. This caused infinite calls.
Solution 1
If you want to call fetchIsProductLiked after products data has been fetched, it is better to compare the old products data with the new one in componentWillReceiveProps as below:
componentWillReceiveProps(nextProps) {
if (nextProps.products !== this.props.products) {
nextProps.products.forEach((product) => {
this.props.fetchIsProductLiked(product.id);
});
}
}
Note: you should start using componentDidUpdate as componentWillReceiveProps is getting deprecated.
Issue 2
isProductLikedData state in reducer doesn't get updated properly.
It is not getting updated because you have used map. Map returns a new array (having elements returned from the callback) of the same length (but you expected to add a new element).
Solution 2
If you want to update the data only when it is not already present in the State, you can use some to check if the data exists. And, push the new data using spread syntax when it returned false:
case "FETCH_IS_PRODUCT_LIKED_SUCCESS": {
return {
...state,
fetching: false,
fetched: true,
isProductLikedData: state.isProductLikedData.some(
(d) => d.productId === action.payload.productId
)
? state.isProductLikedData
: [...state.isProductLikedData, action.payload],
};
}

Redux updating nested data [Immutable Update Patterns]

Can anyone help with this update pattern. I am not using any libraries like immer.
I have to update a nested object and the data looks like dis
Sample data
{
isFetching: false
data:{
nba : {
stack :{
1:[]
}
}
}
}
My Reducer
{
...state,
isFetching: false,
data: {
...state.data,
[action.payload.team]: {
...state[action.payload.team],
[action.payload.framework]: {
...state[action.payload.framework],
[action.payload.build]: action.payload.resp
}
}
}
};
I am able to update until second level but unable to update third child.
can anyone throw a light on where i am missing it.
I put a demo on codesandbox.
https://codesandbox.io/s/todos-0ygrs
Click on collapse and inner collapse items. I am logging the changes for the state in the console below. As you can see at last level, build numbers are getting replaced with the new one's.
Current Behaviour After you expand nba and all the three childs
{
nba: {
stack:{
3:[]
}
}
Expected Behaviour: After you expand stack and all the three childs
{
nba: {
stack:{
1:[],
2:[],
3:[]
}
}
}
You probably have to use a get helper because you may try to set a part of state that doesn't exist yet.
With the get helper you can set the state like this:
const { team, framework, build, resp } = action.payload;
const newState = {
...state,
isFetching: false,
data: {
...get(state, ['data']),
[team]: {
...get(state, ['data', team]),
[framework]: {
...get(state, ['data', team, framework]),
[build]: resp,
},
},
},
};
Somehow i figured out my mistake, Hope it helps someone in future
Initial state should not be null, it should be empty object and update pattern should be in this manner
{
...state,
isFetching: false,
data: {
...state.data,
[action.payload.team]: {
...state.data[action.payload.team],
[action.payload.framework]: {
...state.data[action.payload.team][action.payload.framework],
[action.payload.build]: action.payload.resp
}
}
}
};
if it fails, then try this way
let teamTemp = { ...state.data[action.payload.team]}
{
...state,
isFetching: false,
data: {
...state.data,
[action.payload.team]: {
...teamTemp ,
[action.payload.framework]: {
...teamTemp[action.payload.framework],
[action.payload.build]: action.payload.resp
}
}
}
};
I have forked my codesandbox and updated latest code.
Old Code: https://codesandbox.io/s/todos-0ygrs
New Code: https://codesandbox.io/s/todos-zqeki

Redux overwrites model with previous state

I am currently making a sample project in AngularJs combined with Redux.
I am struggling to get the mappings from the reducer working.
I have a simple input where users can set a new name together with a drop down to select a 'company'.
<input type="text" ng-model="$ctrl.single.object.name">
<select ng-change="$ctrl.getProperties()"
ng-options="option.description as option.description for option in $ctrl.list.all"
ng-model="$ctrl.single.object.company">
When the user changes the company, new properties need to be fetched in order for the user to set these properties.
function FooController($ngRedux, FooActions, BarActions) {
this.$onInit = function () {
this.unsubscribeCompanies = $ngRedux.connect(this.mapStateToThis, BarActions)(this);
this.fetchCompanyList();
};
this.$onDestroy = function () {
this.unsubscribeCompanies();
};
this.fetchCompanyList = function () {
this.fetchCompanies().payload.then((response) => {
this.fetchCompaniesSuccess(response.data);
}, (error) => {
this.fetchCompaniesError(error.data);
});
};
this.getProperties = function () {
this.fetchCompanyProperties(this.single.object.company).payload.then((response) => {
this.fetchCompanyPropertiesSuccess(response.data);
}, (error) => {
this.fetchCompanyPropertiesError(error.data);
});
};
this.mapStateToThis = function (state) {
return {
list: state.bar.list,
single: state.bar.single
};
};
}
module.exports = {
template: require('./index.html'),
controller: ['$ngRedux', 'FooActions', 'BarActions', FooController]
}
The problem I get is that the name and the selected company are overwritten with empty values when the fetch for properties is successful. I get why the values are overwritten with empty values and I have found a way to get it working.
export const GET_COMPANIES = 'GET_COMPANIES';
export const GET_COMPANIES_SUCCESS = 'GET_COMPANIES_SUCCESS';
export const GET_COMPANIES_ERROR = 'GET_COMPANIES_ERROR';
export const GET_COMPANIES_PROPERTIES = 'GET_COMPANIES_PROPERTIES';
export const GET_COMPANIES_PROPERTIES_SUCCESS = 'GET_COMPANIES_PROPERTIES_SUCCESS';
export const GET_COMPANIES_PROPERTIES_ERROR = 'GET_COMPANIES_PROPERTIES_ERROR';
export default function BarActions($http) {
function fetchCompanies() {
return {
type: GET_COMPANIES,
payload: $http.get('api/companies')
};
}
function fetchCompaniesSuccess(companies) {
return {
type: GET_COMPANIES_SUCCESS,
payload: companies
};
}
function fetchCompaniesError(error) {
return {
type: GET_COMPANIES_ERROR,
payload: error
};
}
function fetchCompanyProperties(company) {
return {
type: GET_COMPANIES_PROPERTIES,
payload: $http.get(`api/company/${company}/properties`)
};
}
function fetchCompanyPropertiesSuccess(properties) {
return {
type: GET_COMPANIES_PROPERTIES_SUCCESS,
payload: properties
};
}
function fetchCompanyPropertiesError(error) {
return {
type: GET_COMPANIES_PROPERTIES_ERROR,
payload: error
};
}
return {
fetchCompanies,
fetchCompaniesSuccess,
fetchCompaniesError,
fetchCompanyProperties,
fetchCompanyPropertiesSuccess,
fetchCompanyPropertiesError
}
}
The way I overwrite the values in the reducer is as follows:
import { GET_COMPANIES, GET_COMPANIES_SUCCESS, GET_COMPANIES_ERROR, GET_COMPANIES_PROPERTIES, GET_COMPANIES_PROPERTIES_ERROR, GET_COMPANIES_PROPERTIES_SUCCESS } from "../actions/bar.actions";
const all = [];
const initialState = {
list: {
all,
filtered: all,
error: null,
loading: false
},
single: {
object: {},
error: null,
loading: false
}
};
export function BarReducer(state = initialState, action) {
switch (action.type) {
case GET_COMPANIES:
return { ...state, list: { all: [], filtered: [], error: null, loading: true } };
case GET_COMPANIES_SUCCESS:
return { ...state, list: { all: action.payload, filtered: action.payload, error: null, loading: false } };
case GET_COMPANIES_ERROR:
return { ...state, list: { all: [], filtered: [], error: action.payload.innerException, loading: false } };
case GET_COMPANIES_PROPERTIES:
return { ...state, single: { ...state.single, object: { ...state.single.object }, error: null, loading: true } };
case GET_COMPANIES_PROPERTIES_SUCCESS:
return { ...state, single: { ...state.single, object: { ...state.single.object, payloadValues: action.payload }, error: null, loading: false } };
case GET_COMPANIES_PROPERTIES_ERROR:
return { ...state, single: { object: null, error: action.payload.innerException, loading: false } };
default:
return state;
}
}
The way I now use the spread operator in order to overwrite the old state feels dirty. I was wondering if there are any rules or guidelines to handle this issue. So far I have searched a while on internet and in specific the Redux website but I did not come cross any other solutions.
The breakage is likely due to the structure of the reducer. It is concerned with too many different parts of state and has to operate on deep nested objects, making it easy to accidentally mutate state. The guidelines for reducer structure say that splitting reducer state into normalized slices is the best way to go.
Try splitting your one reducer into multiple smaller reducers. For example:
export const all = (initialAll = [], { type, companies }) => {
switch(type) {
case GET_COMPANIES_SUCCESS: return companies;
default: return initialAll;
}
}
export const error = (initialError = '', { type, error }) => {
switch(type) {
case GET_COMPANIES_ERROR: return error;
default: return initialError;
}
}
export const isFetching = (isFetching = false, { type }) => {
switch(type) {
case GET_COMPANIES: return true;
case GET_COMPANIES_SUCCESS: return false;
case GET_COMPANIES_ERROR: return false;
default: return isFetching;
}
}
Then, compose them into one reducer:
import { combineReducers } from 'redux';
export list = combineReducers({
all,
error,
isFetching
});
// ...
export rootReducer = combineReducers({
list,
single,
// ...
})
This way, each reducer is concerned with only one thing or set of things, and its reduction handlers can do simple operations on single-level state instead of complex operations on deep nested state.
Also, in your list substate, it looks like you are storing the same type of collection resources in both all and filtered with potential overlap. This leads to multiple sources of truth for the same data, which opens the door to data inconsistency. Instead, keep an array of filteredIds:
export const filteredIds = (initialIds = [], { type, filteredIds }) => {
switch(type) {
case SET_FILTERED_IDS: return filteredIds;
default: return initialIds;
}
}
Then, use a selector that filters all by the filteredIds to get your filtered items.
One option is to use Immutable, which would change your reducers to:
case GET_COMPANIES:
return state.setIn(['list', 'loading'], true);
// etc
See Using Immutable.JS with Redux for more information about this approach.
Another option is to use Lodash, as shown in this Issue, you can define the following function to make it similar to the immutable one:
import {clone, setWith, curry} from 'lodash/fp';
export const setIn = curry((path, value, obj) =>
setWith(clone, path, value, clone(obj)),
);
Then you can use setIn as follow:
case GET_COMPANIES:
return setIn(['list', 'loading'], true, state);
// etc
The Lodash approach is just working with plain object, so it might be easier to understand than Immutable.

React-redux: Nested array is being duplicated

I'm trying to create an order table (array) that has a nested "products" array in each order.
The order table is rendering as expected, but the products are the same for every order.
OrderTable Component (simplified for clarity)
class OrderTable extends Component {
componentWillMount() {
this.props.fetchPtOrders(this.props.patient_id);
}
renderOrders(orders) {
return orders.map((order) => {
return (
<tr key={order.id}>
<td>
<ProductTable id={order.id}/>
</td>
</tr>
);
});
}
render() {
const { orders, loading, error } = this.props.orderTable;
return (
<div class="container divcon">
<h1>Orders</h1>
<table class="pto">
<tbody>
{this.renderOrders(orders)}
</tbody>
</table>
</div>
);
}
}
export default OrderTable;
<ProductTable id={orders.id}/> arrays the products and is basically a copy of the above (minus the ProductTable component).
I tried debugging using IDs (3000022 and 3000023) and found that everything is being done in batches.
3000022 contains products / 3000023 is empty.
The response from the requests is being used for both IDs, and is overwritten with every iteration. Only the last response is used for every order.
ProductTable Container:
function mapStateToProps(state, ownProps) {
return {
ProductTable: state.order_products.ProductTable,
order_id: ownProps.id
};
}
const mapDispatchToProps = (dispatch) => {
return {
fetchPtOrderProducts: (id) => {
dispatch(fetchPtOrderProducts(id)).then((response) => {
!response.error ? dispatch(fetchOrderProductsSuccess(response.payload.data)) : dispatch(fetchOrderProductsFailure(response.payload.data));
});
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductTable);
Product Fetch action:
export function fetchPtOrderProducts(id) {
const request = axios({
method: 'get',
url: `${ROOT_URL}/order_product/search.php?s=${id}`,
headers: []
});
return {
type: FETCH_PTORDER_PRODUCTS,
payload: request
};
}
Product Success action:
export function fetchOrderProductsSuccess(order_products) {
console.log("products fetched")
return {
type: FETCH_ORDER_PRODUCTS_SUCCESS,
payload: order_products
};
}
Product Reducers
case FETCH_ORDER_PRODUCTS:// start fetching products and set loading = true
return { ...state, ProductTable: {order_products:[], error: null, loading: true} };
case FETCH_ORDER_PRODUCTS_SUCCESS:// return list of products and make loading = false
return { ...state, ProductTable: {order_products: action.payload, error:null, loading: false} };
How can I make orders.map() and <ProductTable /> array one ID at a time?
Thanks for you help! Sorry if anything is unclear... I'm a complete newbie.
As far as I can see you are overriding the same field in your state.
You should change ProductTable field to maintain order_id for each product list. It should be a map of order_id to order_products.
Note that my code might contain mistakes because I have no runnable example to edit.
Something like this:
ProductTable Container:
function mapStateToProps(state, ownProps) {
return {
orderProductTable: state.order_products.ProductTable[ownProps.id],
order_id: ownProps.id
};
}
const mapDispatchToProps = (dispatch) => {
return {
fetchPtOrderProducts: (id) => {
// Add `id` argument to fetchOrderProductsSuccess here
dispatch(fetchPtOrderProducts(id)).then((response) => {
!response.error ? dispatch(fetchOrderProductsSuccess(id, response.payload.data)) : dispatch(fetchOrderProductsFailure(response.payload.data));
});
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(OrderProductTable);
Product Fetch action:
export function fetchPtOrderProducts(id) {
const request = axios({
method: 'get',
url: `${ROOT_URL}/order_product/search.php?s=${id}`,
headers: []
});
return {
type: FETCH_PTORDER_PRODUCTS,
payload: {id}
};
}
Product Success action:
export function fetchOrderProductsSuccess(id, order_products) {
console.log("products fetched")
// Add `id` field here
return {
type: FETCH_ORDER_PRODUCTS_SUCCESS,
payload: {id, products: order_products}
};
}
Product Reducers
case FETCH_ORDER_PRODUCTS:// start fetching products and set loading = true
return { ...state, ProductTable: {...(state.ProductTable || {}), [action.payload.id]: {order_products:[], error: null, loading: true}}};
case FETCH_ORDER_PRODUCTS_SUCCESS:// return list of products and make loading = false
return { ...state, ProductTable: {...(state.ProductTable || {}), [action.payload.id]: {order_products: action.payload.products, error:null, loading: false}}}};

React Redux how to store http response in Reducer

I have a datatable with "View" Link. Upon clicking, it must fetch data from the backend. But I am having troubles storing the response in React-Redux style.
This is the snippet of the datatable:
$("#personTable").dataTable({
"columns" : [
{ "data" : "id", "name":"id" ,
"render": (data, type, full,meta) => {
return 'View;
}
}
In my routes.jsx, I defined it to forward to PersonForm.jsx
<Route name='personForm' path='/person/view' component={PersonForm}/>
In my PersonForm component, I have this:
componentWillMount() {
let personId = this.props.location.query.id
this.props.onInit(personId)
}
In my PersonFormContainer.jsx:
export const mapDispatchToProps = (dispatch) => {
return {
onInit: (personId) => {
dispatch(init(personId))
}
}
}
This is my PersonActions.jsx:
export function init(personId) {
return function (dispatch) {
httpService.request('/person/view/' + personId, {method: 'get'})
.then((response) => {
console.log(response.data) // response.data is correctly returned
dispatch({
type: "PERSON_INIT",
data: response.data
})
}).catch(err => console.log(err))
}
}
In my PersonReducer.js:
var Immutable = require('immutable');
let initialState =
Immutable.fromJS({
data: {},
fields: {
name: field.create('name', [validation.REQUIRED])
}
})
export default function (state = initialState, action) {
switch(action.type) {
case PERSON_INIT:
return state.set("data", action.data)
//return state.set("fields", Immutable.fromJS(action.fields))
default:
return state
}
}
Now, the problem is back to my PersonForm.jsx. Upon calling render(), data (in my reducer) has some values, but not the fields. I am not sure how to transform the response.data (in my PersonActions) to the fields in my Reducer. Something like this:
if (data) {
for (let key in fields) {
fields[key].value = data[key]
}
}
This is my PersonForm component:
render() {
let store = this.props.person
let fieldsMap = store.get("fields")
<ImmutableInputText label="Name" field={fieldsMap.get("name")}/>
Note: ImmutableInputText is from our templates, something like:
<input id={field.name} className="form-control" name={field.name}
value={this.state.value} onBlur={this.handleBlur} onChange={changeHandler}
disabled={disabled}/>
I am trying to answer this without knowing the structure of the response object, So i will update this answer based on your response.
For now, let's assume this is the response you get from server
{
"code": 200,
"message": "success",
"person": {
"name": "John Doe",
"email": "john.doe#gmail.com",
"dob": "1980-01-01",
"gender": "male",
"status": "active",
}
}
In your PersonReducer.js you can do something like
export default function (state = initialState, action) {
switch(action.type) {
case PERSON_INIT:
return state.set("fields", Immutable.fromJS(action.data.person) )
default:
return state
}
}
but doing this will replace the existing fields object with the data received from server.
If you want to keep all the existing data and only update the data that has been changed or is new.., you can do something like
case PERSON_INIT:
return state.set("fields", Immutable.fromJS({ ...state.get('fields').toObject(), ...action.data.person }) )
If you only want to update the name field, you can do something like
case PERSON_INIT:
return state.setIn(['fields', 'name'], action.data.person.name );
But then you will have to do this for every field and that wont be very effective, So you can make this dynamic by doing
in PersonActions.jsx file (or wherever you want this dynamic field update functionality), you can change the dispatch code to
dispatch({
type: "PERSON_UPDATE_FIELD",
payload: { field: 'name', value: response.data.person.name }
})
Note: I used payload here instead of data, I think its best to follow redux naming conventions, but there's nothing wrong in what you're doing as long as you stick with the same conventions throughout your app.
And in your PersonReducer.js you can have something like
case PERSON_UPDATE_FIELD:
return state.setIn(['fields', action.payload.field ], action.payload.value );
Good luck.

Resources