Redux Reducer nested data - redux

I have been stuck on this concept for a while now. I need to update the 'votes' in an object such as this. What am I doing wrong?
export default function restaurantReducer(state = initialState.restaurants, action) {
switch(action.type) {
case 'UPDATE_RESTAURANT':
return [
...state,
{
...state[action.key],
['votes']: action.userName
}
];
default:
return state;
}
}
The data I have is
{
restaurants: {
ID: {
name : 'name',
votes : {
voterID: 'voters name',
voterID: 'voters name',
}
}
}
}
I would like to be able to add ID's and names to the votes...
initial-state.js is:
const initialState = {
auth: {
status: 'ANONYMOUS',
email: null,
displayName: null,
photoURL: null,
uid: null
},
messages: {
},
users: {
},
restaurants: {
},
};
export default initialState;

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",
});
},
},
});

Getting the categoryId of a post in Graphql

This is my Graphql query for getting posts from headless wordpress:
export const GET_POSTS = gql`
query GET_POSTS( $uri: String, $perPage: Int, $offset: Int, $categoryId: Int ) {
posts: posts(where: { categoryId: $categoryId, offsetPagination: { size: $perPage, offset: $offset }}) {
edges {
node {
id
title
excerpt
slug
featuredImage {
node {
...ImageFragment
}
}
categories {
edges {
node {
categoryId
name
}
}
}
}
}
pageInfo {
offsetPagination {
total
}
}
}
}
${ImageFragment}
`;
When i do this: console.log("DATAAAA", data.posts.edges);
i get:
DATAAAA [
{
node: {
id: 'cG9zdDo0MA==',
title: 'postttt',
excerpt: '<p>dlkfjdsflkdslkdjfkldsf</p>\n',
slug: 'postttt',
featuredImage: null,
categories: [Object],
__typename: 'Post'
},
__typename: 'RootQueryToPostConnectionEdge'
},
{
node: {
id: 'cG9zdDox',
title: 'Hello world!',
excerpt: '<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>\n',
slug: 'hello-world',
featuredImage: null,
categories: [Object],
__typename: 'Post'
},
__typename: 'RootQueryToPostConnectionEdge'
}
]
But when try to go further, inside node, like this: console.log("DATAAAA", data.posts.edges.node); in order to get the categoryId which is inside categories: [Object], i get undefined.
How to get categoryId based on this query?
What i want to do is to get only the posts by a given category in getStaticProps like this, but i dont know how to get that categoryId dynamically. This is what my getStaticProps function looks like:
export async function getStaticProps(context) {
console.log("THE CONTEXT", context);
const { data, errors } = await client.query({
query: GET_POSTS,
variables: {
uri: context.params?.slug ?? "/",
perPage: PER_PAGE_FIRST,
offset: null,
categoryId: <===== How to get this dynamically?
},
});
const defaultProps = {
props: {
data: data || {},
},
revalidate: 1,
};
return handleRedirectsAndReturnData(defaultProps, data, errors, "posts");
}
This is my getStaticPaths function:
export async function getStaticPaths() {
const { data } = await client.query({
query: GET_CATEGORY_SLUGS_ID,
});
const pathsData = [];
data?.categories?.edges.node &&
data?.categories?.edges.node.map((category) => {
if (!isEmpty(category?.slug)) {
pathsData.push({ params: { slug: category?.slug } });
}
});
return {
paths: pathsData,
fallback: FALLBACK,
};
}
and this is what i get from context console.log("THE CONTEXT", context);:
THE CONTEXT {
params: { slug: 'uncategorized' },
locales: undefined,
locale: undefined,
defaultLocale: undefined
}
Any help would be appreciated.

Redirect function in Nuxt middleware is making state null

I have a Nuxt app in which everything works fine in middleware except when I use redirect.
When I comment the redirect('/admin') line it works fine even the state data is present when console logged. As soon as I uncomment the redirect line it makes the state null.
Please help if someone knows this issue. This exact code works in my other projects but not here.
This is my auth.js file in the middleware folder.
export default function ({ store, route, redirect }) {
const user = store.getters['user/user']
const blockRouteAdmin = /\/admin\/*/g
const blockRouteManager = /\/manager\/*/g
const path = ['/signup', '/login']
let value = path.includes(route.path)
if (user) {
if (user.isAdmin) {
if (!route.path.match(blockRouteAdmin)) {
redirect('/admin')
}
}
if (user.isManager) {
if (!route.path.match(blockRouteManager)) {
redirect('/manager')
}
}
if (user.isUser) {
if (
route.path.match(blockRouteAdmin) ||
route.path.match(blockRouteManager) ||
value
) {
console.log('isUser', user.isUser)
redirect('/')
}
}
}
if (!user) {
if (
route.path.match(blockRouteAdmin) ||
route.path.match(blockRouteManager)
) {
redirect('/')
} else {
redirect()
}
}
}
Here is my nuxt.config.js
export default {
// Target: https://go.nuxtjs.dev/config-target
target: 'static',
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'aitl',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' },
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: ['~/plugins/firebaseConfig.js'],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
// https://go.nuxtjs.dev/buefy
'nuxt-buefy',
// https://go.nuxtjs.dev/pwa
'#nuxtjs/pwa',
// https://go.nuxtjs.dev/content
'#nuxt/content',
],
// PWA module configuration: https://go.nuxtjs.dev/pwa
pwa: {
manifest: {
lang: 'en',
},
},
// Content module configuration: https://go.nuxtjs.dev/config-content
content: {},
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {},
}
My index.js inside store.
import { vuexfireMutations } from 'vuexfire'
import { getUserFromCookie } from '../helper/index.js'
export const mutations = {
...vuexfireMutations,
}
export const actions = {
async nuxtServerInit({ dispatch, commit }, { req }) {
try {
const user = getUserFromCookie(req)
if (user) {
await dispatch('user/setUSER', {
email: user.email,
isAdmin: user.admin,
isManager: user.manager,
isUser: user.user,
uid: user.user_id,
name: user.name,
})
}
} catch (err) {
console.log(err)
}
},
}
User.js in store folder
import { auth } from '../plugins/firebaseConfig'
import Cookies from 'js-cookie'
export const state = () => ({
user: null,
})
export const getters = {
user(state) {
return state.user
},
}
export const actions = {
async userlogin({ dispatch }, user) {
try {
const token = await auth.currentUser.getIdToken(true)
const userInfo = {
email: user.email,
isAdmin: user.admin,
isManager: user.manager,
isUser: user.user,
uid: user.uid,
name: user.displayName,
}
Cookies.set('access_token', token)
await dispatch('setUSER', userInfo)
} catch (err) {
console.log(err)
}
},
setUSER({ commit }, user) {
commit('setUSER', user)
},
}
export const mutations = {
setUSER(state, user) {
state.user = user
},
}
The issue was solved by going from target: 'static' to target: 'server', aka mirroring the settings of another working project.

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.

What doe the reduce function do in this #ngrx effects example?

In this #ngrx effects example, what does the reduce function in the following line do?
const newBookEntities = newBooks.reduce((entities: { [id: string]: Book }, book: Book) => {
return Object.assign(entities, {
[book.id]: book
});
}, {});
Can't the newBooks be used in place of newBookEntities?
export function reducer(state = initialState, action: book.Actions | collection.Actions): State {
switch (action.type) {
case book.SEARCH_COMPLETE:
case collection.LOAD_SUCCESS: {
const books = action.payload;
const newBooks = books.filter(book => !state.entities[book.id]);
const newBookIds = newBooks.map(book => book.id);
const newBookEntities = newBooks.reduce((entities: { [id: string]: Book }, book: Book) => {
return Object.assign(entities, {
[book.id]: book
});
}, {});
return {
ids: [ ...state.ids, ...newBookIds ],
entities: Object.assign({}, state.entities, newBookEntities),
selectedBookId: state.selectedBookId
};
}
case book.LOAD: {
const book = action.payload;
if (state.ids.indexOf(book.id) > -1) {
return state;
}
return {
ids: [ ...state.ids, book.id ],
entities: Object.assign({}, state.entities, {
[book.id]: book
}),
selectedBookId: state.selectedBookId
};
}
case book.SELECT: {
return {
ids: state.ids,
entities: state.entities,
selectedBookId: action.payload
};
}
default: {
return state;
}
}
}
It turns this:
[book1,book2,book3]
Into this:
{ '1':book1, '2':book2, '3':book3 }
They are not the same.

Resources