add unseen logic to readonly state redux - react - reactjs

I have normalized events looks like the following:
"entities":{
"events":{
"123":{
... data
}
"124":{
...data
}
}}
I am showing the list on a simple material ui list
now when clicking on a button any new item in the list is now "unseen" and should not be auto-selected
do I need to add "unseen": true/ false. to the basic normalize data? and then dispatch an update each time there is a new event? so it will looks like:
"entities":{
"events":{
"123":{
... data
unseen:false
}
"124":{
...data
unseen: true
}
}}
or it's better to hold a new list of unseen events ?
I am looking for a better way to handle it
do I need to change the read-only state in that case ? because now I am only updating a full object
redux state:
export interface EventsEntities {
events: {
[id: string]: Event;
};
}
export interface EventState {
readonly entities: EventsEntities;
selectedItem: string | null;
readonly result: string[];
}

Every value in the Redux state is assumed to be read-only, so you don't need to remove it in both cases.
You can change the unseen property using a Redux action like
{
type: 'toggle-unseen-event',
eventId: '123',
unseen: true
}
and handle it in reducer like this:
function eventsReducer(state, action) {
switch(action.type) {
...
case 'toggle-unseen-event':
return {
...state,
events: {
...state.events,
[action.eventId]: {
...state.events[action.eventId],
unseen: action.unseen
}
}
}
...
}
}
As you can see, we're not mutating the event objects, so it can be readonly.
The best design for your Redux state depends on how and where you want to consume the data.
If there are components in which you are using events data only (but not unseen), then it's better to keep unseen events list in a separate place in the Redux state so that changing unseen property doesn't cause re-rendering of components which don't care about it.
But if you use unseen property everywhere you are using events data, then it's better to keep them in the same place so that you don't need merging unseen list and events data every time.

Related

How to combine Array / Object reducer

I have following hierarchy for my app:
tabs
filter
tableData
Now tabs is an array. It contains objects - also the filter object.
Example:
[{type:1, filter:{sortBy:publisDate,asc}, order:0, name: 'fun tab'}];
Now my ideal reducer setup would be:
I have a tab reducer controlling active tab, order, name etc.
I have then a filter tab controlling filter updates.
Best case I do not have to worry about "activeTab" etc - but the reducer handles the logic. So when I update the filter it is reflected correctly in the array of tabs.
I could put all of this in one reducer but it is a lot and will end in mess.
What is a good strategy to do this?
I don't know exactly what do you want to do with this tabs/filters, but if everything will be related in the same screen i don't see bad to do everything in one reducer. for instance, you could have something like this:
TabsReducer
{
activeTab: 'products',
filter: {
term : 'sarasa',
order: 0,
sortBy: 'name asc',
}
}
Then, in your reducer if you want to update your filter or something, you can do
if (type === 'CHANGE_FILTER_TERM') {
return {
...state,
filter: {
...state.filter,
term: payload.term
}
}
}

Reducer behavior

As I understand it, when an action is called, all reducers respond. If action exists in the switch case statement of the reducer, it executes. If it doesn't, then the case: default executes which preserves the existing state.
When the action exists in the reducer but the particular property it's trying to update does not exist, it seems to behave OK as there's nothing to update.
For example, I have an action creator that is used to set the visible property of my modals. Each modal has its own Id. My code looks like this:
export default (state = initialState, action) => {
case types.SET_MODAL_IS_VISIBLE:
return Object.assign({}, state,
{ modal22: action.value }
)}
I have the SET_MODAL_IS_VISIBLE in multiple reducers but if modal22 is not defined in a particular reducer, nothing happens and no errors.
Now, I have a scenario that is throwing an error. I have a general purpose date picker component that I built that can be used as a single and independent date picker OR it can be "linked to" another one. The second scenario is useful if I need the user to give me two dates e.g. start and end dates.
I also built a feature where if the date picker is coupled with another one, when the user sets the date in the first date picker, I disable all the dates prior to that date in the second date picker because I don't want the user to unintentionally select an end date that is prior to the start date.
I define my date pickers as below:
const initialState = {
datePickers: {
"startDatePicker": {
activeDate: "8/25/2017",
disabledBefore: "",
linkedTo: "endDatePicker"
},
"endDatePicker": {
activeDate: "",
disabledBefore: "8/25/2017" // This date is set when the user sets the active date in startDatePicker
linkedTo: ""
}
}
}
This scenario is a bit interesting because a state change in one property in my reducer is triggering a state change in another. This is not difficult to do and I have a way of controlling when I do the update.
The action for setting disabled dates looks like below:
...
case types.SET_DISABLED_DATES:
return Object.assign({}, state,
datePickers: Object.assign({}, state.datePickers, {
datePickers[action.datePickerId]: Object.assign({}, state.datePickers[action.datePickerId], {
disabledBefore: action.value
})
})
Please keep in mind that I can and should be able to set disabledBefore even if the date picker is used as an independent one. So, I need my SET_DISABLED_DATES in every reducer.
The problem I'm running into is that whenever I call SET_DISABLED_DATES, I get errors in reducers where the date picker is used as a single/independent one because the date picker Id for its pair is NOT defined in the reducer.
For example, in projectsReducer I may use the date picker as part of a pair so both startDatePicker and endDatePicker are defined and everything works fine.
But I may be using a single instance date picker in the tasksReducer which also responds to the SET_DISABLED_DATES call but it fails because it cannot find the endDatePicker. In this scenario, the tasksReducer is responding to the call I made to set the disabledDates property of endDatePicker in projectsReducer.
I've posted two questions about this already and the only real solution I'm seeing here is that I need to have a condition in my reducer that looks like this:
...
case types.SET_DISABLED_DATES:
if(typeof state.datePickers[action.datePickerId] !== "undefined") { // Making sure that what I'm trying to update exists in the reducer
return Object.assign({}, state,
datePickers: Object.assign({}, state.datePickers, {
datePickers[action.datePickerId]: Object.assign({}, state.datePickers[action.datePickerId], {
disabledBefore: action.value
})
})
} else {
return state;
}
Admittedly, this looks a bit like a kludge but I couldn't really come up with another solution here.
Again, the problem is that for as long as all reducers respond to SET_DISABLED_DATES, it's guaranteed that a particular date picker will not be there and the Object.assign() will throw an error.
Any suggestions? Is the simple condition in the reducer the way to go here? Is it a kludge?
P.S. I tried this code and it works fine and fixes the problem. On the one hand, I feel this is a bit of an anti-pattern but on the other hand, it just seems like a good idea to make sure the property I want to update in my reducer exists before attempting to update it. I'd appreciate your feedback on this. Thanks.
You are just doing basic validation in the reducer before setting the state. That is perfectly fine. I don't think it will be a good practice to check the store in the action creator to prevent dispatching actions on objects not in the store (how would you do that anyway!).
What I don't understand is, how can a datepicker be linked to another datepicker that isn't in the store? Maybe dispatch a create and teardown action on the component's didMount and willUnmount?
I don't know your full requirements but I think we can make it a lot simpler. I'd do something like this:
The store:
{
datePickers: {
id1: {
value: '',
minValue: '',
maxValue: '',
},
id2: {
value: '',
minValue: '',
maxValue: '',
}
}
}
Now, unless you are making some kind of coupled datepicker components that will always behave in pairs, I believe the cleanest approach would be to set the disabled value in the linked datepicker in the mapDispactchToProps function in your parent component.
That is where you would set ids to the components, and you know exactly which component should be disabled before another.
Something like:
dispatch => ({
setArrivalDate(value) {
dispatch(datePickerActions.setValue(arrivalDateId, value);
dispatch(datePickerActions.setMaxValue(depatureDateId, value);
},
setDepatureDate(value) {
dispatch(datePickerActions.setValue(depatureDateId, value);
dispatch(datePickerActions.setMinValue(arrivalDateId, value);
}
})
This may not be abstract enough, but is clean.
You could do the same thing if you have a paired component, but you'd still need to know which date comes before another. It'd be a hassle to make a generic abstraction around it.
Remove the bold part in your code below
...
case types.SET_DISABLED_DATES:
if(typeof state.datePickers[action.datePickerId] !== "undefined") { // Making sure that what I'm trying to update exists in the reducer
return Object.assign({}, state,
datePickers: Object.assign({}, state.datePickers, {
datePickers[action.datePickerId]: Object.assign({}, state.datePickers[action.datePickerId], {
disabledBefore: action.value
})
})
} else {
return state;
}
Also, a little bit of es6 spread and a helper switchcase function makes this code much more readable.
const newReducer = (state = defaultState, action) => switchcase({
[types.SET_DISABLED_DATES]:
state.datePickers[action.datePickerId] === undefined
? state
: ({ ...state,
datePickers: { ...state.datePickers,
[action.datePickerId]: { ...state.datePickers[action.datePickerId],
disabledBefore: action.value,
},
},
}),
})(state)(action.type);
Using lodash/fp/set, the code becomes
const reducerWithLodash = (state = defaultState, action) =>
switchcase({
[types.SET_DISABLED_DATES]:
state.datePickers[action.datePickerId] === undefined
? state
: set({...state}, `datePickers.${action.datePickerId}.disabledBefore`, action.value)
})(state)(action.type)
I haven't tested the lodash version, so please take that with a grain of salt (Dan Abramov seems to approve)

React Parent-Child Relationship and Encapsulation

I have a React design problem that I am trying to solve, and I am running into an issue counter-intuitive to the idea of encapsulation as I try to breakdown an existing component into parent-child to support an additional use case. I am using React 15.3.2 with Redux, ES6, and ImmutableJS. First, I will illustrate the design problem, then I will provide snippets to illustrate why I feel that I have the need to get data back from children and how that is good for encapsulation, and the roadblock I am hitting.
I have read this stackoverflow which has detailed explanation on why passing data from children to parent component is not a good idea,
Pass props to parent component in React.js
But I have some concerns, which I will discuss at the end.
Design:
I am starting with a CheckboxSelect component. The Title bar's text depends on the checked items.
Closed:
Open with selections (current implementation):
To support additional use-case, the dropdown will now open up with more stuff.
Open with selections (new update):
Initial Code:
I am starting with a CheckboxSelect controlled component with the following props interface:
CheckboxSelect.jsx:
CheckboxSelect.propTypes = {
// what title to display by default with no selection
defaultTitle: PropTypes.string.isRequired, ie. "Selected Scopes"
// array of selected options, ie. [{key: "comedy", name: "comedy", checked: false }, ...]
options: PropTypes.array.isRequired,
// handler for when the user checks a selection, this will update
// the reducer state, which causes the options prop to be refreshed and
// passed in from the outer container
onCheck: PropTypes.func.isRequired,
onUncheck: PropTypes.func.isRequired,
onCheckAll: PropTypes.func,
onUncheckAll: PropTypes.func,
className: PropTypes.string,
// controls the open state of the dropdown
open: PropTypes.bool,
// handler for when the user clicks on the dropdown button, this will update the reducer state,
// which causes the open prop to be refreshed and passed in from the outer container
onClick: PropTypes.func,
onCancel: PropTypes.func,
};
// there is currently some logic inside the component to generate the title to display
// produces "comedy, action"
getTitle() {
const { defaultTitle } = this.props;
const checked = this.getChecked();
let fullTitle;
if (this.allChecked() || this.allUnchecked()) {
fullTitle = `${defaultTitle } (${checked.length})`;
} else {
fullTitle = checked.map((option) => option.name).join(', ');
}
return fullTitle;
}
getChecked() {
const { options } = this.props;
return options.filter(option => option.checked);
}
allChecked() {
const { options } = this.props;
return this.getChecked().length === options.length;
}
allUnchecked() {
return this.getChecked().length === 0;
}
ApplicationContainer.jsx (where the component is being used):
scopeDropDownOptions = (currentMovie) => {
// returns [{key: "comedy", name: "comedy", checked: false }]
const applicableScopes = currentMovie.getIn(['attributes', 'applicable_scopes']);
return currentMovie.getIn(['attributes', 'available_scopes']).reduce((result, scope) => {
result.push({
key: scope,
name: scope,
checked: (applicableScopes.includes(scope)),
});
return result;
}, []);
}
onSelectScope = (scope) => {
const movieScopes = this.applicableScopes.push(scope.key);
this.onUpdateField('applicable_scopes', movieScopes);
}
render() {
...
<CheckboxSelect
defaultTitle="Selected Scopes"
options={this.scopeDropdownOptions(currentMovie)}
onCheck={this.onSelectScope}
onUncheck={this.onDeselectScope}
onCheckAll={this.onSelectAllScopes}
onUncheckAll={this.onDeselectAllScopes}
open={store.get('scopeDropdownOpen')}
</CheckboxSelect>
}
New Code:
To support the new layout, I would like to break the existing component into two: a DynamicDropdown that contains CheckboxSelect2 as one of the children, along with any other elements that may be dropped down. Here is how the new code will look inside the ApplicationContainer.
ApplicationContainer.jsx
scopeDropDownOptions = (currentMovie) => {
// returns [{key: "comedy", name: "comedy", checked: false }]
const applicableScopes = currentMovie.getIn(['attributes', 'applicable_scopes']);
return currentMovie.getIn(['attributes', 'available_scopes']).reduce((result, scope) => {
result.push({
key: scope,
name: scope,
checked: (applicableScopes.includes(scope)),
});
return result;
}, []);
}
onSelectScope = (scope) => {
const {store } = this.props;
const cachedApplicableScopes = store.get('cachedApplicableScopes').push(scope.key);
store.get('cachedApplicableScopes').push(scope.key);
this.actions.setCachedApplicableScopes(cachedApplicableScopes);
// wait until apply is clicked before update
}
render() {
...
<DynamicDropdown
className="scope-select"
title={this.scopeDropdownTitle()}
open={store.get('scopeDropdownOpen')}
onClick={this.onScopeDropdownClick}
onCancel={this.onScopeDropdownCancel}
>
<CheckboxSelect2
options={this.scopeDropdownOptions(currentMovie)}
onCheck={this.onSelectScope}
onUncheck={this.onDeselectScope}
onCheckAll={this.onSelectAllScopes}
onUncheckAll={this.onDeselectAllScopes}
visble={store.get('scopeDropdownOpen')}
/>
// ... other children like confirmation message and buttons
</DynamicDropdown>
}
// logic related to CheckboxSelect2 title generation moved to the ApplicationContainer. Not ideal in my opinion as it breaks encapsulation. Further discussions in the Choices section
getScopesChecked() {
const options = this.scopeDropdownOptions(this.currentMovie);
return options.filter(option => option.checked);
}
scopesAllChecked() {
const options = this.scopeDropdownOptions(this.currentMovie);
return this.getScopesChecked().length === options.length;
}
scopesAllUnchecked() {
return this.getScopesChecked().length === 0;
}
scopeDropdownTitle() {
const defaultTitle = "Selected Scopes";
const checked = this.getScopesChecked();
let fullTitle;
if (this.scopesAllChecked() || this.scopesAllUnchecked()) {
fullTitle = `${defaultTitle} (${checked.length})`;
} else {
fullTitle = checked.map((option) => option.name).join(', ');
}
return fullTitle;
}
Problem:
The problem I have is with populating the title props of the DynamicDropdown element with the New Code, since it depends on the result of the CheckboxSelect2 selection.
Keep in mind CheckboxSelect2 is a dumb controlled component, we pass an onCheck handler to it. The this.onSelectScope inside the ApplicationContainer, is responsible for updating the reducer state of what has been checked, which in turn refresh the props and causes the DynamicDropdown and CheckboxSelect2 to be re-rendered.
Choices:
In the old code, there is a group of logic used to figure out the title to display for the dropdown. Here are the choices I am presented with:
To keep encapsulation of letting the CheckboxSelect2 summarize the
title, I tried initially keeping the same title logic inside
CheckboxSelect2, and accessing it via a ref.
ApplicationContainer.jsx
<DynamicDropdown
title={this.childCheckboxSelect.getTitle()}
>
<CheckboxSelect2
ref={(childCheckboxSelect) => this.childCheckboxSelect = childCheckboxSelect}
>
</DynamicDropdown>
At the time that DynamicDropdown is re-rendered, CheckboxSelect2
hasn't received the new props yet from the parent via the one-way
data-flow, so as a child, it actually cannot generate the most
up-to-date title for the DynamicDropdown based on what has been
checked. With this implementation, my title is one-step behind what
was actually checked.
As shown in the ApplicationContainer.jsx for the New Code section
above, the logic for the scopeDropdownTitle could be moved out from
CheckboxSelect2 to ApplicationContainer.jsx, so it sits a level
above DynamicDropdown, enabling the DynamicDropdown to get the
updated title from the reducer state as it renders. While this
solution works, it totally breaks my view on encapsulation, where
the responsibility for determining what title to be displayed, is
something that the CheckboxSelect2 should know about. Basically the
title logic in ApplicationContainer.jsx, now also pre-generates the
options props meant to passed to CheckboxSelect2 to render that
component, the CheckboxSelect2 logic is bleeding into the outer
container.
Let's look at the argument in the stackoverflow post Pass props to parent component in React.js and how it relates to this design problem and an analogy:
"The parent already has that child prop. ...  if the child has a
prop, then it is because its parent provided that prop to the
child!"  Sure, the ApplicationContainer has all the knowledge it
needs to generate the title for the parent DynamicDropdown based on
the checked states of the child CheckboxSelect2, but then is it
really the responsibility of the ApplicationContainer?
Let me give an analogy of a Manager asking an Employee to produce a Report. You can say, the Manager already has all the info, he can surely produce the Report himself! Having a controlled component where the
container manages update to the props for the child via callbacks, seems to me like a Manager passing a bucket to the Employee, the Employee passes back bits and pieces of information in the bucket, and tell the Manager to do the work to summarize things instead of the Employee producing a good summary (which is good encapsulation).
"Additionally, you could have used a ref directly on the child"
I think in the Choices 1) I stated above, using ref does not seem to work when you want up-to-date information from the child as
there is a circular dependency issue with one-way dataflow (ie.
parent needs to get up-to-date summary information from the child,
but the child first depends on the up-to-date information from the
parent).
If you have read this end-to-end and understood the problem, I appreciate your effort! Let me know what you think.

Efficiently computing derived data from react props

We are in the process of implementing performance optimizations in our react/redux application. Part of those optimizations included introducing reselect. This worked nice for data that is derived directly from the state. but what about data that is derived from other props?
Example:
We have 3 components Feed FeedItem and Contact (Contact is a component for displaying a users contact information).
a FeedItem gets an object that represents an item in the feed, one of the properties of a feed item is an actor object. This object is like a user but a bit different (this sucks but can't be changed). This means that if I want to render a Contact for this actor I need to create a new object that maps the properties from an actor to a user. Creating a new object on every render is a performance anti pattern because we are using shallow equality checks.
e.g code:
<Contact
user={{
photoUrl: actor.photo.smallPhotoUrl,
Id: actor.id,
Name: actor.name,
}}
</Contact>
Is there a pattern for solving this? reselect only supports derived data from redux state, this is basically derived data from props.
You can pass whatever you want to reselect's selector methods. It doesn't have to be state and props. That just happens to be it's most common use case. You can call one if it's generated selectors with any number of arguments.
Here's one way you could use it:
function convertActorToContactUser(actor) {
return {
photoUrl: actor.photo.smallPhotoUrl,
Id: actor.id,
Name: actor.name,
};
}
class ActorContact extends Component {
constructor(...args) {
super(...args);
this.getUser = createSelector(
() => this.props.actor,
convertActorToContactUser
);
}
render() {
return <Contact user={this.getUser()} />
}
}

Avoiding event chains with asynchronous data dependencies

The Facebook Flux dispatcher explicitly prohibits ActionCreators from dispatching other ActionCreators. This restriciton is probably a good idea since it prevents your application from creating event chains.
This however becomes an issue as soon as you have Stores containing data from asynchronous ActionCreators that depend on each other. If CategoryProductsStore depends on CategoryStore there doesn't seem to be a way to avoid event chains when without resorting to deferring the follow-up action.
Scenario 1:
A store containing a list of products in a category needs to know from which category ID it should fetch products from.
var CategoryProductActions = {
get: function(categoryId) {
Dispatcher.handleViewAction({
type: ActionTypes.LOAD_CATEGORY_PRODUCTS,
categoryId: categoryId
})
ProductAPIUtils
.getByCategoryId(categoryId)
.then(CategoryProductActions.getComplete)
},
getComplete: function(products) {
Dispatcher.handleServerAction({
type: ActionTypes.LOAD_CATEGORY_PRODUCTS_COMPLETE,
products: products
})
}
}
CategoryStore.dispatchToken = Dispatcher.register(function(payload) {
var action = payload.action
switch (action.type) {
case ActionTypes.LOAD_CATEGORIES_COMPLETE:
var category = action.categories[0]
// Attempt to asynchronously fetch products in the given category, this causes an invariant to be thrown.
CategoryProductActions.get(category.id)
...
Scenario 2:
Another scenario is when a child component is mounted as the result of a Store change and its componentWillMount/componentWillReceiveProps attempts to fetch data via an asynchronous ActionCreator:
var Categories = React.createClass({
componentWillMount() {
CategoryStore.addChangeListener(this.onStoreChange)
},
onStoreChange: function() {
this.setState({
category: CategoryStore.getCurrent()
})
},
render: function() {
var category = this.state.category
if (category) {
var products = <CategoryProducts categoryId={category.id} />
}
return (
<div>
{products}
</div>
)
}
})
var CategoryProducts = React.createClass({
componentWillMount: function() {
if (!CategoryProductStore.contains(this.props.categoryId)) {
// Attempt to asynchronously fetch products in the given category, this causes an invariant to be thrown.
CategoryProductActions.get(this.props.categoryId)
}
}
})
Are there ways to avoid this without resorting to defer?
Whenever you are retrieving the state of the application, you want to be retrieving that state directly from the Stores, with getter methods. Actions are objects that inform Stores. You could think of them as being like a request for a change in state. They should not return any data. They are not a mechanism by which you should be retrieving the application state, but rather merely changing it.
So in scenario 1, getCurrent(category.id) is something that should be defined on a Store.
In scenario 2, it sounds like you are running into an issue with the initialization of the Store's data. I usually handle this by (ideally) getting the data into the stores before rendering the root component. I do this in a bootstrapping module. Alternatively, if this absolutely needs to be async, you can create everything to work with a blank slate, and then re-render after the Stores respond to an INITIAL_LOAD action.
For scenario 1:
I would dispatch new the action from the view itself, so a new action -> dispatcher -> store -> view cycle will trigger.
I can imagine that your view needs to retrieve the category list and also it has to show, by default, the list of products of the first category.
So that view will react to changes con CategoryStore first. Once the category list is loaded, trigger the new Action to get the products of the first category.
Now, this is the tricky part. If you do that in the change listener of the view, you will get an invariant exception, so here you have to wait for the payload of the first action to be completely processed.
One way to solve this is to use timeout on the change listener of the view. Something similar to what is explained here:
https://groups.google.com/forum/#!topic/reactjs/1xR9esXX1X4 but instead of dispatching the action from the store, you would do it from the view.
function getCategoryProducts(id) {
setTimeout(() => {
if (!AppDispatcher.isDispatching()) {
CategoryProductActions.get(id);
} else {
getCategoryProducts(id);
}
}, 3);
}
I know, it is horrible, but at least you won't have stores chaining actions or domain logic leaking to action creators. With this approach, the actions are "requested" from the views that actually need them.
The other option, which I haven't tried honestly, is to listen for the DOM event once the component with the list of categories is populated. In that moment, you dispatch the new action which will trigger a new "Flux" chain. I actually think this one is neater, but as said, I haven't tried yet.

Resources