Imagine a UI with two React components:
<FilterContainer />
<UserListContainer />
We pull down an array of users from the server:
[
{
name: 'John',
enjoys: ['Sailing', 'Running']
},
{
name: 'Bob',
enjoys: ['Running', 'Eating']
},
{
name: 'Frank',
enjoys: ['Sailing', 'Eating']
}
]
The UI looks a little like this:
Filter: Sailing Running Eating
UserList:
John
Frank
You can click on either a filter or a user. To get to this stage, we've clicked on 'Sailing' and then on 'Frank' (maybe we see a nice photo of Frank in the middle of the screen).
My Redux state, built using combineReducers, looks like this:
{
ui: {
filter: {enjoys: 'Sailing'},
userList: {selected: 'John'}
}
data: [user array]
}
I have two actions, SELECT_USER and SELECT_FILTER.
When I click on a filter (SELECT_FILTER fires), I want the ui.userList.selected to persist if that user is still in the filter, and the ui.userList.selected to be set to null if the user is not in the filter.
So if I now click on Eating, I'll see a list with Bob and Frank in it, and Frank is selected. But if I click on Running, I'll see John and Bob, but neither are selected.
However I'm struggling to do this in the conventional Redux methodology. When the userList reducer sees the SELECT_FILTER action, there's no way for it to check the data state to see if the currently selected user is still in that filter condition or not.
What's the right way to do this?
function filter(state = {enjoys: null}, action) {
switch (action.type) {
case SELECT_FILTER:
return {
...state,
enjoys: action.enjoys
}
default:
return state
}
}
function userList(state = {selected: null}, action) {
switch (action.type) {
case SELECT_USER:
return {
...state,
selected: action.name
}
default:
return state
}
}
const ui = combineReducers({
filter,
userList
})
let initialUsers = [
{
name: 'John',
enjoys: ['Sailing', 'Running']
},
{
name: 'Bob',
enjoys: ['Running', 'Eating']
},
{
name: 'Frank',
enjoys: ['Sailing', 'Eating']
}
]
const rootReducer = combineReducers({
ui,
data: (state=initialUsers) => state // in reality, loaded from server
})
export default rootReducer
Reducer should be aware only of a small part of state.
Good place for described logic is the action creator. With redux-thunk you will be able to make a decision based on a global state.
function selectFilter(enjoys) {
return (dispatch, getState) => {
dispatch({type: SELECT_FILTER, enjoys});
// if getState().ui.userList.selected exists in getState().data
dispatch({type: SELECT_USER, name: null});
}
};
You need another action for this.
If you are filtering the users in the data reducer, you will need to dispatch an action in one of your components' hooks (componentWillUpdate or componentWillReceiveProps) when you detect that the array of users has changed. This action will provide your filter reducer with the current array of users, and there you can set the selected field as you like.
If you are filtering the users in the server, I guess you already have an action like FETCH_USERS_SUCCESS that you can use for this.
It should be handled by the filter reducer. You need to send the users data as part of the action payload. Hence the reducer could Calc the selected user logic. You should consider adding a new action, as #cuttals suggested.
Related
I'm using react-redux to fetch data from MongoDB database and put it into React App.
I've following structure to work upon:
const initialState = {
Level: [{
wId: Math.random(),
Level1: [
{
id: Math.random(),
item1: 'item1',
item2: 'item2'
},
.......
],
Level2: [
{
id: Math.random(),
item1: 'item1',
item2: 'item2'
},
.......
]
}]
}
Redux Function:
export default function (state = initialState, action) {
switch (action.type) {
case GET_ITEMS:
return {
...state,
// what should i write here to get above mentioned state structure
}
..............
}
Note:
Initially Level is empty. So if new data is received in payload then the following structure should be formed.
How to update particular item like item1 at level2
Sample Input:
action.payload = {
id1: 23234, // should be assigned in Wid
ITEM: [ // Level1
{
id2: 89724, // should be assigned in Level1.id
i: 'abc', // should be assigned in Level1.item1
j: 'xyz' // should be assigned in Level1.item2
}
]
}
I you dont know how many items you are going to get its would be difficult. One way to work around this issue could compare the previos state with current state and update only necessary part that got changed.
You can use number of libraries or follow any answer in How to determine equality for two JavaScript objects? to compare the objects.
Ideally you would need different actions to update Level, Level ->Level 1 and so on.
Create separate actions for adding levels. Call that action when on user events which add a level to your initial state.
export default function (state = initialState, action) {
switch (action.type) {
case GET_ITEMS:
return {
...state,
// what should i write here to get above mentioned state structure
}
case ADD_LEVELS:
return {
...state,
Level: [...state.Level, action.payload.Level]
}
}
You can move the id generation logic to the component as it will make your life simpler.
Many links and tutorials advice to group the logic in action creators in order to simplify the reducer logic.
Imagine a simple (normalized) state:
const initialState = {
parent: {
allIds: [0],
byId: {
0: {
parentProperty: `I'm the parent`,
children: [1, 2]
}
}
},
children: {
allIds: [1, 2],
byId: {
1: {
childrenProperty: `I'm the children`
},
2: {
childrenProperty: `I'm the children`
}
}
}
}
I would now delete the parent. Since only the parent refers to the children, I would also delete the children too.
I imagine a action creator like this:
function deleteParents(parentId) {
return {type: 'DELETE_PARENT', payload: {parentId}};
}
and
function deleteChild(childId) {
return {type: 'DELETE_CHILD', payload: {childId}};
}
For now, to manage this case I do something like this (with redux-thunk)
function deleteParentAndChildren(parentId) {
return (dispatch, getState) {
const childrenIds = myChildrenSelector(getState(), parentId);
const deleteChildrenActions = childrenIds.map(deleteChild);
const deleteParentAndChildrenAction = batchActions([
deleteParents(parentId),
...deleteChildrenActions
], 'DELETE_PARENT_AND_CHILDREN');
dispatch(deleteParentAndChildrenAction);
}
}
In that way, I compose little action into big one, and the reducer logic is very simple because it only consist to delete a key in an object.
Conversely, I don't like to use redux-thunk (used to async actions) just to get the state (and this is considered as anti pattern).
How do you guys manage this type of problems ?
Does a tool like redux-sage may help ?
The problematic you seem to have seems fairly common and if your application is a bit sophisticated I would suggest using redux-orm which is a bit hard to understand and then integrate but once set up it just abstract you all the hard relationship work between your entities.
I opine differently here. The way I would do it is, I would delete child while deleting parent but not y dispatching child_delete action. When you create new state while deleting parent in reducer, at that time, you can access the children referred by that parent, and remove those as well from state.
Also, if you still want to dispatch actions separately, you can pass childids from component to action. From that action, you can dispatch two different actions, deleting parent and chilren ids.
----------- EDIT -------
// The initial application state
const initialState = {
parent: {
allIds: [0],
byId: {
0: {
parentProperty: `I'm the parent`,
children: [1, 2]
}
}
},
children: {
allIds: [1, 2],
byId: {
1: {
childrenProperty: `I'm the children`
},
2: {
childrenProperty: `I'm the children`
}
}
}
}
export default function batchManagement(state = initialState, action) {
switch (action.type) {
case 'DELETE_PARENT': //assuming action.deleteParents = [0]
//Here you have access to state, so you can change parents and children as well, and create a totally new state
case 'DELETE_CHILDREN':
//Return a new object for state
default:
return state;
}
}
I have a subtitle file, structured as follows:
{ id: '54',
startTime: '00:03:46,572',
endTime: '00:03:53,160',
text: 'Hello this is a text'
},
{ id: '55',
startTime: '00:03:53,160',
endTime: '00:03:58,799',
text: 'To be precise, the subtitle file of a movie'
},
I would now like to go through this file and show the subtitles whenever the respective time is reached in a playing video. I do know roughly how I would realise something like this in javascript, but I am wondering how to do it using React & Redux.
Could I somehow save the time of the video playing in my state and then, everytime that changes, react accordingly with my subtitles? Or what would you suggest? I would very much welcome some code / examples.
This question feel way too general.
The way I would approach the problem is following. Hopefully it gives you a general idea.
I presume subtitles is an array of objects and ids correspond to array indexes.
const initialState = {
activeSubtitleId: 0,
// Here are your subtitles
subtitles: [],
}
Then setup a reducer listening to one action
export const videoReducer = (state = initialState, action) => {
switch(action.type) {
case SYNC_SUBTITLES: {
const { videoTime } = action.payload
const activeSubtitleId = state.subtitles
.filter(subtitle => subtitle.startTime >= videoTime && subtitle.endTime <= videoTime)[0].id
return { ...state, activeSubtitleId }
}
}
}
export default videoReducer
Last missing piece is an action SYNC_SUBTITLES
export const SYNC_SUBTITLES = "SYNC_SUBTITLES"
export const syncSubtitles = (videoTime) => ({
type: SYNC_SUBTITLES,
payload: {
videoTime
}
})
To wire it all up you need to connect redux store to your component, and extract state.subtitles[state.activeSubtitleId]. You might some memoizing library for this, for example Reselect. Depending of how often will you like to sample the time.
Now you can display the correct subtitles.
To keep them synced, you need to dispatch SYNC_SUBTITLES action from your component with defined tick frequency. You can use setTimeout for this, or built-in videoplayer tick emitter.
So the connect might look like this. Pretty silly, but working.
#connect((state, props) => {
const activeSubtitleId = state.activeSubtitleId;
const subtitles = state.subtitles;
return {
activeSubtitle: subtitles[activeSubtitleId]
}
}, {
syncSubtitles
})
For a react component I'm writing I'm using redux to manage the state of the component. I haven't really used redux before so I've been running into some issues with one of the reducers i've written. One of the methods in my react component makes a http request then dispatches the data to the reducer through a simple action function. However, I have another action written for this reducer to delete entries from the state, but whenever I use this action the reducer returns a empty array. Any help is appreciated.
reducer:
export function Weeklystate(state=[], action){
switch(action.type){
case 'ADD_PROVIDER':
return [...state, action.arr];
case 'REMOVE_PROVIDER':
const newstate = [...state];
return newstate.filter((provider) => provider[0].full_name.toLowerCase().indexOf(action.str) === -1);
default:
return state;
}
}
actions.js:
export function addProvider(arr){
return {
type: 'ADD_PROVIDER',
arr
};
}
export function removeProvider(str){
return {
type: 'REMOVE_PROVIDER',
str
};
}
Component methods:
getData(){
const { url, queries } = this.state;
let data_arr = [];
const que = queries.map(function(query){
let postquery = {
full_name: query.name,
date_start: query.startdate,
date_end: query.enddate
};
return axios.post(url, postquery);
});
axios.all(que).then((responses) => {
responses.forEach((response) => {
this.props.addProvider(response.data);
});
});
}
componentWillMount(){
this.getData();
this.props.removeProvider('Example Name here');
const { rawdata } = this.props;
return console.log(this.props);
}
MaptoProps functions:
function mapStateToPropsBETA(state){
return {
rawdata: state.rawdata,
parseddata: state.parseddata
};
}
function mapDispatchtoProps(dispatch){
return bindActionCreators({
addProvider: addProvider,
removeProvider: removeProvider
}, dispatch);
}
export default connect(mapStateToPropsBETA, mapDispatchtoProps)(Weekly_Beta);
stores.js:
const WeeklyStore = createStore(reducer);
WeeklyStore.subscribe(() => {
console.log(WeeklyStore.getState());
});
export default WeeklyStore;
sample data:
[
[
{
first_name: "First Name Here...",
last_name: "Last Name Here...",
full_name: "Full Name Here",
date: "2016-01-17",
charges: {
cosmetic: 25000.00,
medical: 25000.00,
total: 50000.00
},
payments: 75000.00,
appointments: 99,
pk: 5
},
{
first_name: "First Name Here...",
last_name: "Last Name Here...",
full_name: "Full Name Here",
date: "2016-01-24",
charges: {
cosmetic: 25000.00,
medical: 25000.00,
total: 50000.00
},
payments: 75000.00,
appointments: 99,
pk: 5
},
],
[
{
first_name: "First Name Here...",
last_name: "Last Name Here...",
full_name: "Full Name Here",
date: "2016-01-17",
charges: {
cosmetic: 25000.00,
medical: 25000.00,
total: 50000.00
},
payments: 75000.00,
appointments: 99,
pk: 5
},
{
first_name: "First Name Here...",
last_name: "Last Name Here...",
full_name: "Full Name Here",
date: "2016-01-24",
charges: {
cosmetic: 25000.00,
medical: 25000.00,
total: 50000.00
},
payments: 75000,
appointments: 99,
pk: 5
},
]
];
UPDATE:
I did some testing and I got some bizarre results. I executed the same code inside of the console on chrome and my everything ran just fine. However, the reducer is still returning a empty array.
UPDATE #2:
After some further troubleshooting I think I might have a better idea of what's causing the state to write improperly. I was able to get the spread operator working so data immutability isn't much of an issue at this point.
However, there's two things that I happened to notice when troubleshooting. The component props that hold my redux state are not being updated when the redux state changes. To confirm this I used the subscribe method to give me the state whenever a action is dispatched. The field for rawdata is populated with the data I'm looking for in my redux state, however, the data is not being pushed to the equivalent component prop.
The second thing I noticed is that the REMOVE_PROVIDER action isn't dispatching as the subscribe method doesn't report anything back in the console. I'm not sure why this is happening, but I think the filter perhaps may be the cause as the redux state remains untouched whenever I try dispatching the action.
At this point I'm convinced that the problems I'm having are more of react-redux issue rather than being a problem with reducer itself. I've also taken the liberty of updating my code snippets to reflect what I'm seeing.
log of props from react component:
log entries from redux store:
If your example is accurate, then just change the REMOVE_PROVIDER case to
case 'REMOVE_PROVIDER':
return newstate.filter((provider)=> provider[0].full_name.indexOf(action.data) === -1);
As it's written in your question, the filter is returning all providers who have the name "Example Name Here". That's none of them. You want the opposite, that is, to return all of the providers whose names don't match the entered name.
Firstly, your ADD_PROVIDER is mutating the state directly, adding newstate = state doesn't copy the state array, as it only creates an reference to the state.
For your ADD_PROVIDER, what is action.data? According to your actions, itself is already an array, yet you are wrapping it with another array notation, your state becomes array of arrays of array. -____-?.
As the other answer suggested, your REMOVE_PROVIDER logic is wrong, you are keeping all the prodivers whose fullname has action.data.
To fix, your reducer should be:
export function Weeklystate(state=[], action){
switch(action.type){
case 'ADD_PROVIDER':
return [...state, action.data]; // this returns a new array
case 'REMOVE_PROVIDER':
// this will keep provider whose full_name does not have action.data
return state.filter((provider)=>provider[0].full_name.indexOf(action.data.full_name) === -1);
default:
return state;
}
}
As my second point suggest, I am not sure how you want your state to look like, so fix accordingly. With this code, I assume ADD_PROVIDER action.data is an array with 1 element.
Normalizr is great at creating structured JSON repositories of entities.
We have many cases displaying lists of data e.g. posts that have been normalised. Where posts are listed the API response is limited to a few key fields.
We also have cases where we display one of these posts although we now need to fetch the FULL JSON entity from the API with all the fields.
How is it best to deal with this?
A a seperate reducer, thunk/saga, selectors and actions?
B simply insert the extended version of thepost fetched from the API into the reducer. Reusing the selectors etc from before?
Think of the app's state as a database. I suggest you to use this state shape:
{
entities: {
// List of normalized posts without any nesting. No matter whether they have all fields or not.
posts: {
'1': {
id: '1',
title: 'Post 1',
},
'2': {
id: '2',
title: 'Post 2',
}
},
},
// Ids of posts, which need to displayed.
posts: ['1', '2'],
// Id of full post.
post: '2',
}
First of all, we are creating our normalizr schemas:
// schemas.js
import { Schema, arrayOf } from 'normalizr';
const POST = new Schema('post');
const POST_ARRAY = arrayOf(POST);
After success response, we are normalizing response data and dispatching the action:
// actions.js/sagas.js
function handlePostsResponse(body) {
dispatch({
type: 'FETCH_POSTS',
payload: normalize(body.result, POST_ARRAY),
});
}
function handleFullPostResponse(body) {
dispatch({
type: 'FETCH_FULL_POST',
payload: normalize(body.result, POST),
});
}
In reducers, we need to create entities reducer, which will be listening all actions and if it has entities key in payload, would add this entities to the app state:
// reducers.js
import merge from 'lodash/merge';
function entities(state = {}, action) {
const payload = action.payload;
if (payload && payload.entities) {
return merge({}, state, payload.entities);
}
return state;
}
Also we need to create corresponding reducers to handle FETCH_BOARDS and FETCH_FULL_BOARD actions:
// Posts reducer will be storing only posts ids.
function posts(state = [], action) {
switch (action.type) {
case 'FETCH_POSTS':
// Post id is stored in `result` variable of normalizr output.
return [...state, action.payload.result];
default:
return state;
}
}
// Post reducer will be storing current post id.
// Further, you can replace `state` variable by object and store `isFetching` and other variables.
function post(state = null, action) {
switch (action.type) {
case 'FETCH_FULL_POST':
return action.payload.id;
default:
return state;
}
}
I agree with both of your two choices and would have come to the same conclusion. But let's have a closer look at them to see an advantage form one over the other:
(B) You can merge the post entities (preview and full representation) as one entity in your reducer, but you would keep track of the result arrays (preview and full representation), which you would get from the normalizr normalized data after the API requests. Then you can easily distinguish afterwards, if you already have the full representation of the post. Your sub-state might look like the following:
const postState = {
// merged results from PREVIEW api
previews: [1, 2, 3],
// merged results from FULL api
full: [2],
// all merged entities
entities: {
1: {
title: 'foo1'
},
2: {
title: 'foo2',
body: 'bar',
},
3: {
title: 'foo3'
}
}
};
(A) You would have two reducers + actions, one for each representation, to distinguish the entities. Depending on the PREVIEW or FULL posts API request, you would serve one of your reducers via one explicit action. Your sub-states might look like these:
const previewPostState = {
// merged results from PREVIEW api
result: [1, 2, 3],
// all preview entities
entities: {
1: {
title: 'foo1'
},
2: {
title: 'foo2',
},
3: {
title: 'foo3'
}
}
};
const fullPostState = {
// merged results from FULL api
result: [2],
// all full entities
entities: {
2: {
title: 'foo2',
body: 'bar'
}
}
};
From a very high level perspective you can already see that you would have to save duplicated information. The post entity with id: 2 would be saved two times with its title property: one time for previewPostState and one time for fullPostState. Once you want to change the title property in your global state, you would have to do it at two places. One would violate the single source of truth in Redux. That's the reason I would go with choice (B): You have one place for your post entities, but can distinguish clearly their representations by your result arrays.