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.
Related
I am trying to set new values by redux but a single key "status" every time after dispatching as an empty string. I don't know what is it and why every time empty maybe somebody know solution?
call action and dispatch code:
store.dispatch(actionSetNewResponseFilterValues({
active_level: "1",
query_in: ["0"],
status: ["15"]})
);
action method:
export const actionSetNewResponseFilterValues = (newValues) => {
return {
type: SET_RESP_FILT_VALS,
newValues
}
}
and code in reducer:
case SET_RESP_FILT_VALS:
case LOAD_RESP_FILT_VALS:
return {
...state,
filter: {
...state.filter,
newValues: {
...state.filter.newValues,
...action.newValues
}
}
}
#udpated
when in reducer coding whithout destructuring-assignment:
case SET_RESP_FILT_VALS:
case LOAD_RESP_FILT_VALS:
return {
...state,
filter: {
...state.filter,
newValues: action.newValues
}
}
in dispatching I see an empty status key:
I'm kind of new to React.js & Redux, so I have encountered a problem with Reducers.
I am creating a site that have a main "Articles" page, "Question & Answers" page, I created for each one a separate Reducer that both work just fine.
The problem is in "Main Page" which contains a lot of small different pieces of information, and I don't want to create each little different piece of information its on Reducer, so I am trying to create one Reducer which will handle a lot of very small different pieces of information, and I can't make that work, inside the main "Content" object, I put 2 Key Value Pairs that each have an array, one for each different information, one is "Features" info, and one for the "Header" info.
This is the error that I'm getting:
Uncaught TypeError: Cannot read property 'headerContent' of undefined
at push../src/reducers/ContentReducer.js.__webpack_exports__.default (ContentReducer.js:15)
I am not sure what's the problem, maybe my code is wrong or maybe my use of the spread operator, any solution?
I have added the necessary pages from my code:
ACTIONS FILE
export const addFeatureAction = (
{
title = 'Default feature title',
feature = 'Default feature',
} = {}) => ({
type: 'ADD_FEATURE',
features: {
id: uuid(),
title,
feature
}
})
export const addHeaderAction = (
{
title = 'Default header title',
head = 'Default header',
} = {}) => ({
type: 'ADD_HEADER',
header: {
id: uuid(),
title,
head
}
})
REDUCER FILE:
const defaultContentReducer = {
content: {
featuresContent: [],
headerContent: [],
}
}
export default (state = defaultContentReducer, action) => {
switch(action.type) {
case 'ADD_FEATURE':
return [
...state.content.featuresContent,
action.features
]
case 'ADD_HEADER':
return [
...state.content.headerContent,
action.header
]
default:
return state
}
}
STORE FILE:
export default () => {
const store = createStore(
combineReducers({
articles: ArticleReducer,
qnaList: QnaReducer,
content: ContentReducer
})
);
return store;
}
The reducer function is supposed to return the next state of your application, but you are doing a few things wrong here, you are returning an array, a piece of the state and not the state object, I would suggest you look into immer to prevent this sort of errors.
Simple fix:
export default (state = defaultContentReducer, action) => {
switch(action.type) {
case 'ADD_FEATURE':
return {...state, content: {...state.content. featuresContent: [...action.features, ...state.content.featuresContent]}}
// More actions are handled here
default:
return state
}
}
If you use immer, you should have something like this
export default (state = defaultContentReducer, action) => {
const nextState = produce(state, draftState => {
switch(action.type) {
case 'ADD_FEATURE':
draftState.content.featuresContent = [...draftState.content.featuresContent, ...action.features]
});
break;
default:
break;
return nextState
}
After DELETE a resource via a Button I update the same resource with a GET, i get the response with the correct data but the redux store doesn't update (apparently the redux action see no diff), and so the props are not updated.
But when I do a CREATE and then a GET like before (same call) this time the redux store see the diff and update the props.
The resource are the same between the 2 calls (after DELETE or CREATE), and the calls are even the same. Why in one case the redux store see the diff and don't in another case ?
crudCreate(`${CAMPAIGNS}/${id}/${LINE_ITEMS}`, data, () =>
crudGetAll(LINE_ITEMS, {}, { campaignId: id }, 1000, () => this.displayModal()),
);
crudDelete(LINE_ITEMS, lineItem.id, { id }, lineItem, () =>
crudGetAll(LINE_ITEMS, {}, { campaignId: id }, 1000, ({ payload: { data } }) =>
this.updateLineItems(data),
),
);
I don't any error on Redux debugger or on Console. I have a custom DataProvider, in that case it only redirect for the good routes
case LINE_ITEMS: {
if (type === 'DELETE') {
return { urn: `${apiUrl}/campaigns/${filter.id}/${resource.toLowerCase()}/${id}` };
}
if (filter) {
if ('campaignId' in filter) {
return {
urn: `${apiUrl}/campaigns/${filter.campaignId}/${resource.toLowerCase()}`,
filter: {
...filter,
campaignId: undefined,
},
};
}
}
return {
urn: `${apiUrl}/campaigns/${id.campaignId}/${resource.toLowerCase()}/${data.lineItemId}`,
};
}
And I only use the reducer from react-admin, here my index.js:
import { connect } from 'react-redux';
import { crudDelete, crudGetAll, translate } from 'react-admin';
export default connect(
state => ({
lineItems: Object.values(state.admin.resources.lineItems.data) || {},
swordData: Object.values(state.admin.resources.sword.data),
}),
{ crudDelete, crudGetAll },
)(withRouter(WithPermissions(translate(MyComponent))));
Does anybody have an idea? Thanks for your help
I am using react-admin and I need to control directly the store from one resource, in my case, the orders resource.
Everytime I run the GET_LISTit appends the new records in the list from the store, but, I would like to get a new list from the server and discard the old ones. Here`s where I retrieve the records:
dataProvider(GET_LIST, 'orders', {
filter: { updatedAt: filterDate }, // Get date from Filter.
sort: { field: 'updatedAt', order: 'DESC' },
pagination: { page: 1, perPage: 999 },
}).then(response => response.data)
So, I decided to manipulate the store directly and after some digging I saw this answer and this code from the source:
const dataReducer: Reducer<RecordSetWithDate> = (
previousState = initialState,
{ payload, meta }
) => {
if (meta && meta.optimistic) {
if (meta.fetch === UPDATE) {
const updatedRecord = {
...previousState[payload.id],
...payload.data,
};
return addRecords([updatedRecord], previousState);
}
if (meta.fetch === UPDATE_MANY) {
const updatedRecords = payload.ids.map(id => ({
...previousState[id],
...payload.data,
}));
return addRecords(updatedRecords, previousState);
}
if (meta.fetch === DELETE) {
return removeRecords([payload.id], previousState);
}
if (meta.fetch === DELETE_MANY) {
return removeRecords(payload.ids, previousState);
}
}
if (!meta || !meta.fetchResponse || meta.fetchStatus !== FETCH_END) {
return previousState;
}
switch (meta.fetchResponse) {
case GET_LIST:
case GET_MANY:
case GET_MANY_REFERENCE:
return addRecords(payload.data, previousState);
case GET_ONE:
case UPDATE:
case CREATE:
return addRecords([payload.data], previousState);
default:
return previousState;
}
};
So, based on that, I created a custom action to delete the old ids from my list and add the new ones retrieved from the data source:
import {GET_LIST, DELETE_MANY, FETCH_END } from 'react-admin';
export const UPDATE_ORDER_ADMIN = 'UPDATE_ORDER_ADMIN';
export const update_orders_admin = (data, oldIDS) => ({
type: UPDATE_ORDER_ADMIN,
payload: { data, ids: oldIDS },
meta: {
resource: 'orders',
optimistic: true,
fetch: DELETE_MANY,
fetchResponse: GET_LIST,
fetchStatus: FETCH_END,
},
});
And I am using this custom action after retrieve data from the backend:
dataProvider(GET_LIST, 'orders', {
filter: { updatedAt: filterDate }, // Get date from Filter.
sort: { field: 'updatedAt', order: 'DESC' },
pagination: { page: 1, perPage: 999 },
}).then(response => response.data)
.then(data => {
const ids = orders ? Object.keys(orders) : [];
update_orders_admin(data, ids);
this.setState({ isLoading: false })
return null;
});
However, the system is calling the DELETE action from backend, trying to delete the records from the database, while, what I would like is just delete these records from my view.
Any thoughts?
In your custom action you have the fetch set as DELETE_MANY which will do a loop over every id performing DELETE operation. Not sure if you implementation will work, but the current error is about that. You could try to remove the fetch ans see what happens, but I think without it he will not fetch records. If I'm not mistaken RA only adds new ids to data, however if data changed in the meantime I don't think it will replace the changed data for that you need to reimplement the data provider to change the update data behaviour which is similar to what you're trying.
I have a form that is per-populated based on user saved information and the user can change those information at any time.
A simple example of how this is structured is:
componentWillMount() {
const { user, getProfile } = this.props;
if (user.profileId) {
getProfile(user.profileId); // this is a redux action that fetch for user profile data updating the props.
}
}
onChangeValue(e) {
const { updateProfileField } = this.props; // this is a redux action to update a single input
const { id, value } = e.target;
updateProfileField(id, value);
}
Inputs looks like this:
<InputField id="screenName" type="text" value={profile.screenName} onChangeValue={this.onChangeValue} placeholder="Your username" />
The InputField component is the following:
class InputField extends Component {
constructor(props) {
super(props);
this.state = {
value: props.value,
};
this.onChangeValue = this.onChangeValue.bind(this);
}
onChangeValue(value) {
this.props.onChangeValue(value);
}
render() {
const {
id, type, parent,
} = this.props;
return (
<input
id={id}
parent-id={parent}
className="form-control"
type={type}
name={id}
value={this.state.value}
onChange={this.onChangeValue}
/>
);
}
}
The Redux action to handle the updateProfileField is:
export function updateProfileField(field, value) {
const payload = { field, value };
return dispatch => dispatch({payload, type: types.UPDATE_PROFILE_FIELD});
}
Finally the Reducer:
const initialState = {
data: {}, // Here are stored the profile information like `screenName`
...
};
export default (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
...
case types.UPDATE_PROFILE_FIELD:
// Here my attempts to update the input value
return {
...state,
data: { ...state.data, [payload.field]: payload.value },
};
// Immutability helper
// return update(state, {
// data: { [payload.field]: { $set: payload.value } },
// });
// return Object.assign({}, state.data, {
// [payload.field]: payload.value,
// });
}
}
The point is that everything works just fine as soon as I load the page and I start to type the information in my form.
If I change something in the code, it reload automatically using HMR, I can see the information typed before in the inputs but if I try to update the information it doesn't work anymore...
I can see the "state" updating just the last letter of the input and the UI seems freeze. (ie.: input value is: 'foo', I type: 'bar' the result is: foob, fooa, foor... and it is not reflected in the UI).
I think I'm doing something wrong in here...