Redux immutable pattern - redux

I use react with redux.
Action:
export const updateClicked = (id, section) => {
return {
type: actionTypes.UPDATE_CLICKED,
id,
section
};
};
Please advise the best way to immutable update property in nested array:
Reducer:
const initialState = {
updates: {
html: {
id: 'html',
label: 'HTML',
count: 0,
items: [
{
id: 1,
label: 'Header',
price: 10,
bought: false
},
{
id: 2,
label: 'Sidebar',
price: 50,
bought: false
}
]
}
}
};
My action:
action = {
id: 1,
bought: true
}
I want to update bought property inside items array. I.e.:
const updateClicked= (state, action) => {
const updateSections = state.updates[action.section].items;
const updatedItems = updateSections.map(el => {
if (el.id === action.id && !el.bought) {
el.bought = true;
}
return el;
});
//How to update state???
return {}
};
Will be glad if you explain 2 ways to do this:
With es6 spread operator
With some library (like immutability-helper)
Thanks!

With es6 spread operator:
export default (state = initialState, action) => {
if (action.type !== actionTypes.UPDATE_CLICKED) return state;
return {
...state,
updates: {
...state.updates,
html: {
...state.updates.html,
items: state.updates.html.items.map((item, idx) => idx === action.id
? {...item, bought: item.bought}
: item
)
}
}
}
};

Related

Call other slice methods from same slice in redux

I am trying to call other slice methods from the same slice in redux. I tried this approach but TS is throwing errors. What am I doing wrong? (Please see function setInitialLocation)
export const appSlice = createSlice({
name: "app",
initialState,
reducers: {
addCompletedAppSetupSteps: (
state,
action: PayloadAction<AppSetupSteps | AppSetupSteps[]>
) => {
if (Array.isArray(action.payload)) {
state.data.completedAppSetupSteps = union(
state.data.completedAppSetupSteps,
action.payload
);
} else {
state.data.completedAppSetupSteps = union(
state.data.completedAppSetupSteps,
[action.payload]
);
}
},
selectLocation(state, action: PayloadAction<LocationSelection | null>) {
if (!action.payload) {
state.data.selectedLocation = null;
return;
}
const payloadWithDefaults: LocationSelection = {
...action.payload,
settings: {
hasPopup: action.payload?.settings?.hasPopup ?? true,
hasFlyToAnimation:
action.payload?.settings?.hasFlyToAnimation ?? true,
},
};
state.data.selectedLocation = payloadWithDefaults;
},
setInitialLocation(state, action: PayloadAction<LocationSelection>) {
appSlice.caseReducers.selectLocation(state, action); // I am trying to call the other methods here.
appSlice.caseReducers.addCompletedAppSetupSteps(state, {
payload: AppSetupSteps.SetInitialLocationSelection,
type: "addCompletedAppSetupSteps",
});
},
},
});

How to watch the whole pinia state inside the store?

I can't watch the whole pinia state inside the store. If I do it Vue js will show a next message: [Vue warn] Avoid app logic that relies on enumerating keys on a component instance. The keys will be empty in production mode to avoid performance overhead. I know about the tip in pinia documentation, but I can't do it inside the store.
My store:
export const achievementsKey = 'achievements'
export const missCountKey = 'missCount'
export const useAchievementStore = defineStore('achievement', () => {
const store = useStore()
const skinStore = useSkinStore()
const achievements = ref<Achievements>(
localStorage.getItem(achievementsKey)
? JSON.parse(<string>localStorage.getItem(achievementsKey))
: [
{
name: 'Get into the top ten',
prize: skinStore.skins[0],
done: false,
},
{ name: 'Miss 5 times', prize: 100, done: false },
{ name: 'Earn 2000 points', prize: 150, done: false },
{ name: 'Buy a new skin', prize: 150, done: false },
]
)
const missCount = ref(
localStorage.getItem(missCountKey)
? Number(localStorage.getItem(missCountKey))
: 0
)
function accomplish(achievement: Achievement): void {
achievement.done = true
if (typeof achievement.prize === 'number') {
store.balance += achievement.prize
}
}
watch(
achievements,
() => {
localStorage.setItem(achievementsKey, JSON.stringify(achievements.value))
},
{ deep: true }
)
watch(missCount, () => {
/** Accomplish an achievement */
if (missCount.value >= 5) {
const achievement = achievements.value.find(
(item) => item.name === 'Miss 5 times'
)
if (achievement && !achievement.done) {
accomplish(achievement)
}
}
localStorage.setItem(missCountKey, JSON.stringify(missCount.value))
})
return { achievements, missCount, accomplish }
})
// Show a vue warn
// const achievementStore = useAchievementStore()
// watch(
// achievementStore,
// (state) => {
// localStorage.setItem('piniaState', JSON.stringify(state))
// },
// { deep: true }
// )

How to display data from firebase in vis.js timeline

I m using vis.js timeline and i want display date from firestore. It works when I type manually (look --> this.items), but does not work with firestore (look --> this.users).
I m using Vue framework.
<script>
export default {
data() {
return {
users: [],
items: [
{
id: '1',
content: 'London',
group: 'Mike',
start: '2021-12-20',
end: '2022-06-19',
},
],
}
},
async fetch() {
await this.loadPlaces()
},
methods: {
async loadPlaces() {
const querySnapshot = await getDocs(collection(db, 'places'))
querySnapshot.forEach((doc) => {
this.users.push({ id: doc.id, ...doc.data() })
})
this.$store.commit('places/setPlaces', this.users)
},
},
computed: {
places() {
return this.$store.state.places.places
},
},
mounted() {
let container = document.getElementById('visualization')
let options = {
moveable: true,
}
let timeline = new vis.Timeline(container)
timeline.setOptions(options)
timeline.setGroups(this.groups)
timeline.setItems(this.items)
},
}
</script>
I found a solution.
I just moved all code from mounted() to method loadPlaces (under this.$store.commit)
Save yourself trouble and use the vis datasets instead.
my pinia store in vue 3 looks like this.
import { defineStore } from 'pinia'
import { DataSet } from 'vis-data/esnext'
export const useVisData = defineStore('visData', {
state: () => ({
items: new DataSet([]),
groups: new DataSet([]),
selectedItems: [],
serializedGroupsAndItems: []
}),
actions: {
//Group actions
showAllGroups() {
this.groups.forEach(group => {
this.groups.updateOnly({ id: group.id, visible: true })
});
},
addGroup(group) {
this.groups.add(group)
},
hideGroup(group) {
this.groups.updateOnly({ id: group, visible: false })
},
//Item actions
addItem(item) {
this.items.add(item)
},
removeItem(item) {
this.items.remove(item)
},
setSelectedItems(items) {
this.selectedItems = items
},
//data add/remove
serializeData() {
this.serializedGroupsAndItems.push({
groups: JSON.stringify(this.groups.get()),
items: JSON.stringify(this.items.get())
})
},
loadSerializedData() {
this.clearGroupsAndItems()
this.serializedGroupsAndItems.forEach(data => {
this.addGroup(JSON.parse([data.groups]))
this.addItem(JSON.parse([data.items]))
})
},
//misc
clearGroupsAndItems() {
this.groups.clear()
this.items.clear()
}
},
getters: {
getHiddenGroups(state) {
return state.groups.get({
filter: (item) => {
return item.visible === false
}
})
}
}
})
Also remember to watch for changes in your options.
Might be better to wrap it in a vue component too. something like this.
this is what i did.
let timeline;
const visref = ref(null);
onMounted(async () => {
timeline = new Timeline(visref.value, props.items, props.groups, {...props.options, ...timelineOptions});
props.events.forEach(event => {
on(event, (properties) => {
// console.log(event, properties)
emits(`vis${event}`, properties);
});
});
})
<template>
<div ref="visref"></div>
</template>
then you can use it like so:
const timelineref = ref();
<Timeline
ref="timelineref"
:items="visStore.items"
:groups="visStore.groups"
:options="options"
/>
remember to expose the instance in your timeline component then you can call the functions using a ref like this.
timelineref.value.timeline.zoomOut(0.5)

Handling loading state of multiple async calls in an action/reducer based application

I donĀ“t think this issue is bound to a specific framework or library, but applies to all store based application following the action - reducer pattern.
For clarity, I am using Angular and #ngrx.
In the application I am working on we need to track the loading state of individual resources.
The way we handle other async requests is by this, hopefully familiar, pattern:
Actions
GET_RESOURCE
GET_RESOURCE_SUCCESS
GET_RESOURCE_FAILURE
Reducer
switch(action.type)
case GET_RESOURCE:
return {
...state,
isLoading = true
};
case GET_RESOURCE_SUCCESS:
case GET_RESOURCE_FAILURE:
return {
...state,
isLoading = false
};
...
This works well for async calls where we want to indicate the loading state globally in our application.
In our application we fetch some data, say BOOKS, that contains a list of references to other resources, say CHAPTERS.
If the user wants to view a CHAPTER he/she clicks the CHAPTER reference that trigger an async call. To indicate to the user that this specific CHAPTER is loading, we need something more than just a global isLoading flag in our state.
The way we have solved this is by creating a wrapping object like this:
interface AsyncObject<T> {
id: string;
status: AsyncStatus;
payload: T;
}
where AsyncStatus is an enum like this:
enum AsyncStatus {
InFlight,
Success,
Error
}
In our state we store the CHAPTERS like so:
{
chapters: {[id: string]: AsyncObject<Chapter> }
}
However, I feel like this 'clutter' the state in a way and wonder if someone has a better solution / different approach to this problem.
Questions
Are there any best practices for how to handle this scenario?
Is there a better way of handling this?
I have faced several times this kind of situation but the solution differs according to the use case.
One of the solution would be to have nested reducers. It is not an antipattern but not advised because it is hard to maintain but it depends on the usecase.
The other one would be the one I detail below.
Based on what you described, your fetched data should look like this:
[
{
id: 1,
title: 'Robinson Crusoe',
author: 'Daniel Defoe',
references: ['chp1_robincrusoe', 'chp2_robincrusoe'],
},
{
id: 2,
title: 'Gullivers Travels',
author: 'Jonathan Swift',
references: ['chp1_gulliverstravels', 'chp2_gulliverstravels', 'chp3_gulliverstravels'],
},
]
So according to your data, your reducers should look like this:
{
books: {
isFetching: false,
isInvalidated: false,
selectedBook: null,
data: {
1: { id: 1, title: 'Robinson Crusoe', author: 'Daniel Defoe' },
2: { id: 2, title: 'Gullivers Travels', author: 'Jonathan Swift' },
}
},
chapters: {
isFetching: false,
isInvalidated: true,
selectedChapter: null,
data: {
'chp1_robincrusoe': { isFetching: false, isInvalidated: true, id: 'chp1_robincrusoe', bookId: 1, data: null },
'chp2_robincrusoe': { isFetching: false, isInvalidated: true, id: 'chp2_robincrusoe', bookId: 1, data: null },
'chp1_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp1_gulliverstravels', bookId: 2, data: null },
'chp2_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp2_gulliverstravels', bookId: 2, data: null },
'chp3_gulliverstravels': { isFetching: false, isInvalidated: true, id: 'chp3_gulliverstravels', bookId: 2, data: null },
},
}
}
With this structure you won't need isFetching and isInvalidated in your chapter reducers as every chapter is a separated logic.
Note: I could give you a bonus details later on on how we can leverage the isFetching and isInvalidated in a different way.
Below the detailed code:
Components
BookList
import React from 'react';
import map from 'lodash/map';
class BookList extends React.Component {
componentDidMount() {
if (this.props.isInvalidated && !this.props.isFetching) {
this.props.actions.readBooks();
}
}
render() {
const {
isFetching,
isInvalidated,
data,
} = this.props;
if (isFetching || (isInvalidated && !isFetching)) return <Loading />;
return <div>{map(data, entry => <Book id={entry.id} />)}</div>;
}
}
Book
import React from 'react';
import filter from 'lodash/filter';
import { createSelector } from 'reselect';
import map from 'lodash/map';
import find from 'lodash/find';
class Book extends React.Component {
render() {
const {
dispatch,
book,
chapters,
} = this.props;
return (
<div>
<h3>{book.title} by {book.author}</h3>
<ChapterList bookId={book.id} />
</div>
);
}
}
const foundBook = createSelector(
state => state.books,
(books, { id }) => find(books, { id }),
);
const mapStateToProps = (reducers, props) => {
return {
book: foundBook(reducers, props),
};
};
export default connect(mapStateToProps)(Book);
ChapterList
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import map from 'lodash/map';
import find from 'lodash/find';
class ChapterList extends React.Component {
render() {
const { dispatch, chapters } = this.props;
return (
<div>
{map(chapters, entry => (
<Chapter
id={entry.id}
onClick={() => dispatch(actions.readChapter(entry.id))} />
))}
</div>
);
}
}
const bookChapters = createSelector(
state => state.chapters,
(chapters, bookId) => find(chapters, { bookId }),
);
const mapStateToProps = (reducers, props) => {
return {
chapters: bookChapters(reducers, props),
};
};
export default connect(mapStateToProps)(ChapterList);
Chapter
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import map from 'lodash/map';
import find from 'lodash/find';
class Chapter extends React.Component {
render() {
const { chapter, onClick } = this.props;
if (chapter.isFetching || (chapter.isInvalidated && !chapter.isFetching)) return <div>{chapter.id}</div>;
return (
<div>
<h4>{chapter.id}<h4>
<div>{chapter.data.details}</div>
</div>
);
}
}
const foundChapter = createSelector(
state => state.chapters,
(chapters, { id }) => find(chapters, { id }),
);
const mapStateToProps = (reducers, props) => {
return {
chapter: foundChapter(reducers, props),
};
};
export default connect(mapStateToProps)(Chapter);
Book Actions
export function readBooks() {
return (dispatch, getState, api) => {
dispatch({ type: 'readBooks' });
return fetch({}) // Your fetch here
.then(result => dispatch(setBooks(result)))
.catch(error => dispatch(addBookError(error)));
};
}
export function setBooks(data) {
return {
type: 'setBooks',
data,
};
}
export function addBookError(error) {
return {
type: 'addBookError',
error,
};
}
Chapter Actions
export function readChapter(id) {
return (dispatch, getState, api) => {
dispatch({ type: 'readChapter' });
return fetch({}) // Your fetch here - place the chapter id
.then(result => dispatch(setChapter(result)))
.catch(error => dispatch(addChapterError(error)));
};
}
export function setChapter(data) {
return {
type: 'setChapter',
data,
};
}
export function addChapterError(error) {
return {
type: 'addChapterError',
error,
};
}
Book Reducers
import reduce from 'lodash/reduce';
import { combineReducers } from 'redux';
export default combineReducers({
isInvalidated,
isFetching,
items,
errors,
});
function isInvalidated(state = true, action) {
switch (action.type) {
case 'invalidateBooks':
return true;
case 'setBooks':
return false;
default:
return state;
}
}
function isFetching(state = false, action) {
switch (action.type) {
case 'readBooks':
return true;
case 'setBooks':
return false;
default:
return state;
}
}
function items(state = {}, action) {
switch (action.type) {
case 'readBook': {
if (action.id && !state[action.id]) {
return {
...state,
[action.id]: book(undefined, action),
};
}
return state;
}
case 'setBooks':
return {
...state,
...reduce(action.data, (result, value, key) => ({
...result,
[key]: books(value, action),
}), {});
},
default:
return state;
}
}
function book(state = {
isFetching: false,
isInvalidated: true,
id: null,
errors: [],
}, action) {
switch (action.type) {
case 'readBooks':
return { ...state, isFetching: true };
case 'setBooks':
return {
...state,
isInvalidated: false,
isFetching: false,
errors: [],
};
default:
return state;
}
}
function errors(state = [], action) {
switch (action.type) {
case 'addBooksError':
return [
...state,
action.error,
];
case 'setBooks':
case 'setBooks':
return state.length > 0 ? [] : state;
default:
return state;
}
}
Chapter Reducers
Pay extra attention on setBooks which will init the chapters in your reducers.
import reduce from 'lodash/reduce';
import { combineReducers } from 'redux';
const defaultState = {
isFetching: false,
isInvalidated: true,
id: null,
errors: [],
};
export default combineReducers({
isInvalidated,
isFetching,
items,
errors,
});
function isInvalidated(state = true, action) {
switch (action.type) {
case 'invalidateChapters':
return true;
case 'setChapters':
return false;
default:
return state;
}
}
function isFetching(state = false, action) {
switch (action.type) {
case 'readChapters':
return true;
case 'setChapters':
return false;
default:
return state;
}
}
function items(state = {}, action) {
switch (action.type) {
case 'setBooks':
return {
...state,
...reduce(action.data, (result, value, key) => ({
...result,
...reduce(value.references, (res, chapterKey) => ({
...res,
[chapterKey]: chapter({ ...defaultState, id: chapterKey, bookId: value.id }, action),
}), {}),
}), {});
};
case 'readChapter': {
if (action.id && !state[action.id]) {
return {
...state,
[action.id]: book(undefined, action),
};
}
return state;
}
case 'setChapters':
return {
...state,
...reduce(action.data, (result, value, key) => ({
...result,
[key]: chapter(value, action),
}), {});
},
default:
return state;
}
}
function chapter(state = { ...defaultState }, action) {
switch (action.type) {
case 'readChapters':
return { ...state, isFetching: true };
case 'setChapters':
return {
...state,
isInvalidated: false,
isFetching: false,
errors: [],
};
default:
return state;
}
}
function errors(state = [], action) {
switch (action.type) {
case 'addChaptersError':
return [
...state,
action.error,
];
case 'setChapters':
case 'setChapters':
return state.length > 0 ? [] : state;
default:
return state;
}
}
Hope it helps.

meteor react-autosuggest - Missing class properties transform

I'm using Meteor React and trying to get react-autosuggest working (from the basic usage example https://github.com/moroshko/react-autosuggest
The error I get is Missing class properties transform - it's happening at onChange (right after this.state.
this.state = {
value: '',
suggestions: getSuggestions('')
};
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
onSuggestionsUpdateRequested = ({ value }) => {
this.setState({
suggestions: getSuggestions(value)
});
};
I've looked around to see if I could find a solution, but no luck.
Hopefully someone can shed some light on what's going on.
Using class Example extends React.Component {...} doesn't work with meteor. Try this approach instead:
import React from 'react';
import Autosuggest from 'react-autosuggest';
import AutosuggestHighlight from 'autosuggest-highlight';
Example = React.createClass({
getInitialState() {
return {
value: '',
suggestions: [],
};
},
onChange(event, { newValue }) {
this.setState({
value: newValue,
});
},
onSuggestionsFetchRequested({ value }) {
this.setState({
suggestions: this.getSuggestions(value),
});
},
onSuggestionsClearRequested() {
this.setState({
suggestions: [],
});
},
getSuggestionValue(suggestion) {
return suggestion.name;
},
getSuggestions(value) {
const languages = [
{
name: 'C',
year: 1973,
},
{
name: 'C#',
year: 2001,
},
{
name: 'C++',
year: 1984,
},
{
name: 'Clojure',
year: 2008,
},
{
name: 'Elm',
year: 2013,
},
{
name: 'Go',
year: 2010,
},
];
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
if (inputLength === 0) {
return [];
}
return languages.filter(language =>
language.name.toLowerCase().slice(0, inputLength) === inputValue
);
},
renderSuggestion(suggestion, { query }) {
const suggestionText = `${suggestion.name} (${suggestion.year})`;
const matches = AutosuggestHighlight.match(suggestionText, query);
const parts = AutosuggestHighlight.parse(suggestionText, matches);
return (
<span className="suggestion-content">
<span>
{
parts.map((part, index) => {
const className = part.highlight ? 'highlight' : null;
return (
<span className={className} key={index}>{part.text}</span>
);
})
}
</span>
</span>
);
},
render() {
const { value, suggestions } = this.state;
const inputProps = {
value,
placeholder: 'Enter programming language...',
onChange: this.onChange,
};
return (
<div>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
inputProps={inputProps} />
</div>
);
},
});
Then simply use <Example /> to render the autosuggest field.
This was tested with Meteor 1.4.1.1

Resources