Redux and Calendar Repeat Events

What should be the correct way to store / handle repeating events in a redux store?

Problem: let's say that we have a backend API that generates repeating events using complex business logic. Some of the events may have the same identifier. Suppose the generated output is as follows:

[ { "id": 1, "title": "Weekly meeting", "all_day": true, "starts_at": "2017-09-12", "ends_at": "2017-09-12" }, { "id": 3, "title": "Daily meeting1", "all_day": false, "starts_at": "2017-09-12", "ends_at": "2017-09-12", }, { "id": 3, "title": "Daily meeting1", "all_day": false, "starts_at": "2017-09-13", "ends_at": "2017-09-13", }, { "id": 3, "title": "Daily meeting1", "all_day": false, "starts_at": "2017-09-14", "ends_at": "2017-09-14", } ] 

Possible solution: create a unique identifier by adding the following uid property as follows: id + # + starts_at . Thus, we could uniquely identify each event. (I'm using it right now)

An example :

 [ { "id": 1, "uid": "1#2017-09-12", "title": "Weekly meeting", "all_day": true, "starts_at": "2017-09-12", "ends_at": "2017-09-12" } ] 

I am wondering if there is another way, maybe more elegant, than composing a unique identifier?

+5
source share
4 answers

In the end, this is what I implemented (for demo purpose only - no related code is missing):

eventRoot.js:

 import { combineReducers } from 'redux' import ranges from './events' import ids from './ids' import params from './params' import total from './total' export default resource => combineReducers({ ids: ids(resource), ranges: ranges(resource), params: params(resource) }) 

events.js:

 import { GET_EVENTS_SUCCESS } from '@/state/types/data' export default resource => (previousState = {}, { type, payload, requestPayload, meta }) => { if (!meta || meta.resource !== resource) { return previousState } switch (type) { case GET_EVENTS_SUCCESS: const newState = Object.assign({}, previousState) payload.data[resource].forEach(record => { // ISO 8601 time interval string - // http://en.wikipedia.org/wiki/ISO_8601#Time_intervals const range = record.start + '/' + record.end if (newState[record.id]) { if (!newState[record.id].includes(range)) { // Don't mutate previous state, object assign is only a shallow copy // Create new array with added id newState[record.id] = [...newState[record.id], range] } } else { newState[record.id] = [range] } }) return newState default: return previousState } } 

There is also a data reducer, but it is associated with the parent reducer because of a universal implementation that is reused for general list responses. Event data is updated and the start / end property is deleted because it is composed by a range ( ISO 8601 interval time line ). This can be later used by moment.range or split on '/' to get the start / end data. I selected an array of range strings to make it easier to check for existing ranges as they can increase. I think that a primitive string comparison (including indexOf or es6) will be faster than a loop in a complex structure in such cases.

data.js (stripped down version):

 import { END } from '@/state/types/fetch' import { GET_EVENTS } from '@/state/types/data' const cacheDuration = 10 * 60 * 1000 // ten minutes const addRecords = (newRecords = [], oldRecords, isEvent) => { // prepare new records and timestamp them const newRecordsById = newRecords.reduce((prev, record) => { if (isEvent) { const { start, end, ...rest } = record prev[record.id] = rest } else { prev[record.id] = record } return prev }, {}) const now = new Date() const newRecordsFetchedAt = newRecords.reduce((prev, record) => { prev[record.id] = now return prev }, {}) // remove outdated old records const latestValidDate = new Date() latestValidDate.setTime(latestValidDate.getTime() - cacheDuration) const oldValidRecordIds = oldRecords.fetchedAt ? Object.keys(oldRecords.fetchedAt).filter(id => oldRecords.fetchedAt[id] > latestValidDate) : [] const oldValidRecords = oldValidRecordIds.reduce((prev, id) => { prev[id] = oldRecords[id] return prev }, {}) const oldValidRecordsFetchedAt = oldValidRecordIds.reduce((prev, id) => { prev[id] = oldRecords.fetchedAt[id] return prev }, {}) // combine old records and new records const records = { ...oldValidRecords, ...newRecordsById } Object.defineProperty(records, 'fetchedAt', { value: { ...oldValidRecordsFetchedAt, ...newRecordsFetchedAt } }) // non enumerable by default return records } const initialState = {} Object.defineProperty(initialState, 'fetchedAt', { value: {} }) // non enumerable by default export default resource => (previousState = initialState, { payload, meta }) => { if (!meta || meta.resource !== resource) { return previousState } if (!meta.fetchResponse || meta.fetchStatus !== END) { return previousState } switch (meta.fetchResponse) { case GET_EVENTS: return addRecords(payload.data[resource], previousState, true) default: return previousState } } 

This can be used by a calendar component with an event selector:

 const convertDateTimeToDate = (datetime, timeZoneName) => { const m = moment.tz(datetime, timeZoneName) return new Date(m.year(), m.month(), m.date(), m.hour(), m.minute(), 0) } const compileEvents = (state, filter) => { const eventsRanges = state.events.list.ranges const events = [] state.events.list.ids.forEach(id => { if (eventsRanges[id]) { eventsRanges[id].forEach(range => { const [start, end] = range.split('/').map(d => convertDateTimeToDate(d)) // You can add an conditional push, filtered by start/end limits events.push( Object.assign({}, state.events.data[id], { start: start, end: end }) ) }) } }) return events } 

And this is what the data structure in the reduction tools looks like:

https://i.imgur.com/5nzrG6e.png

Each time events are retrieved, their data is updated (if there is a change), and links are added. Here is a screenshot of decux diff after selecting a range of new events:

enter image description here

Hope this helps someone, I’ll just add that this is not a battle, but more proof of the concept that works.

[EDIT] I will probably translate part of this logic to the backend, since then there will be no need to split / join / delete properties.

0
source

There is a possible error with your current solution. What happens if the id and start_id of the two events are the same? Is this possible in your domain?

Because of this, I usually use this pretty lib in such cases. It creates very short unique identifiers that have some good properties, such as guarantees not to overlap, unpredictable and so on.

Also ask yourself if you really need unique identifiers in your case. It looks like your back-end is not able to distinguish between events anyway, so why bother? The Redux store will happily save your event without a uid .

+1
source

Maybe not a big part of the improvement (if at all), but just using JSON.stringify to check for duplicates can make the unique id obsolete.

 const existingEvents = [ { "id": 3, "title": "Daily meeting1", "all_day": false, "starts_at": "2017-09-14", "ends_at": "2017-09-14", } ]; const duplicate = { "id": 3, "title": "Daily meeting1", "all_day": false, "starts_at": "2017-09-14", "ends_at": "2017-09-14", }; const eventIsDuplicate = (existingEvents, newEvent) => { const duplicate = existingEvents.find(event => JSON.stringify(event) == JSON.stringify(newEvent)); return typeof duplicate != 'undefined'; }; console.log(eventIsDuplicate(existingEvents, duplicate)); // true 

I assume that this will be preferable only for your existing solution, if for some reason you want to keep all the uniqueness logic on the client side.

+1
source

As far as I understand the examples you indicated, it seems that the server sends a specific event whenever the details of the event change.

If so, and you want to track changes in events, your form can be an array of objects with all event fields that contain current data, and a history property that is an array of all previous (or n most recent) event objects and timestamps by which they were received. Here's what your gearboxes look like, keeping only the last five event changes for each event. I expect the action to have a payload property that has a standard event property and a timestamp property that can be easily done in the action creator.

 const event = (state = { history: [] }, action) => { switch (action.type) { case 'EVENT_FETCHED': return ({ ...action.payload.event, history: [...state.history, action.payload].slice(-5), }); default: return state; } }; const events = (state = { byID: {}, IDs: [] }, action) => { const id = action.payload.event.ID; switch (action.type) { case 'EVENT_FETCHED': return id in state.byID ? { ...state, byID: { ...state.byID, [id]: event(state.byID[id], action) }, } : { byID: { ...state.byID, [id]: event(undefined, action) }, IDs: [id], }; default: return state; } }; 

You do not need a unique identifier for this. Please let me know if I misunderstood your problem.

Edit: this is a small extension of pattern in the Redux documentation to store previous events.

+1
source

Source: https://habr.com/ru/post/1271251/


All Articles