Look-ups in Redux Reducers - reactjs

Let's say I have the following state:
state = {
products: {
50: {
sku: "000",
name: "A Product",
category: 123,
...
}
},
categories: {
123: {
name: "Some Category",
parentCategory: 100,
department: "Electronics"
}
},
filteredProducts: [50]
}
I want to be able to filter products based on categories. However, I need to filter based on multiple properties of categories. i.e. I might want to get all categories within the Electronics department or I might want to get a category with id 123 and all it's sub-categories.
This is a bit of a contrived example that closely matches what I'm trying to achieve but it's a bit easier to understand, so please bear with me. I'm aware that in this specific instance, I could probably use something like reselect, but assuming that I needed to do a category lookup for a products reducer, what would my options be?

You can use reselect as you mentioned, and make some selectors with parameter the re-use these selectors from categories in products to be as follow:
Make your category/selectors file as follow:
import { createSelector } from 'reselect';
const categoriesSelector = state => state.categories;
const selectCategoryById = id => {
return createSelector(
categoriesSelector,
categories => categories[id]
);
}
const selectCategoryByName = name => {
return createSelector(
categoriesSelector,
categories => categories.filter(c => c.name === name)
);
}
export default {
categoriesSelector,
selectCategoryById,
selectCategoryByName,
}
Meanwhile, in product/selector you can import both category and product selector files as follow:
import { createSelector } from 'reselect';
import { selectCategoryById } from './category/selectors';
const productsSelector = state => state.products;
const selectProductByCategoryId = id => {
return createSelector(
productsSelector,
selectCategoryById,
(products, categories) => products.filter(p.category.indexOf(id) > -1)
);
}
export default {
productsSelector,
selectProductByCategoryId,
}
And in product/reducer, you can import both selectors and return the new changed state based on category logic.

Related

useSelector only returning initial state

I'm building a simple review app with react and redux toolkit.
Reviews are added via a form in AddReview.js, and I'm wanting to display these reviews in Venue.js.
When I submit a review in AddReview.js, the new review is added to state, as indicated in redux dev tools:
However when I try to pull that state from the store in Venue.js, I only get the initial state (the first two reviews), and not the state I've added via the submit form:
Can anyone suggest what's going wrong here?
Here's how I've set up my store:
store.js
import { configureStore } from "#reduxjs/toolkit";
import reviewReducer from '../features/venues/venueSlice'
export const store = configureStore({
reducer:{
reviews: reviewReducer
}
})
Here's the slice managing venues/reviews:
venueSlice.js
import { createSlice } from "#reduxjs/toolkit";
const initialState = [
{id:1, title: 'title 1',blurb: 'blurb 1'},
{id:2, title: 'title 2',blurb: 'blurb 2'}
]
const venueSlice = createSlice({
name: 'reviews',
initialState,
reducers: {
ADD_REVIEW: (state,action) => {
state.push(action.payload)
}
}
})
export const { ADD_REVIEW } = venueSlice.actions
export default venueSlice.reducer
And here's the Venue.js component where I want to render reviews:
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
const Venue = () => {
const { id } = useParams()
const reviews = useSelector((state) => state.reviews)
console.log(reviews)
return (
<div>
{reviews.map(item => (
<h1>{item.title}</h1>
))}
</div>
)
}
export default Venue;
Form component AddReview.js
import { useState } from "react"
import { useDispatch } from "react-redux"
import { ADD_REVIEW } from "./venueSlice"
import { nanoid } from "#reduxjs/toolkit"
const AddReview = () => {
const [ {title,blurb}, setFormDetails ] = useState({title:'', blurb: ''})
const dispatch = useDispatch()
const handleChange = (e) => {
const { name, value } = e.target
setFormDetails(prevState => ({
...prevState,
[name]: value
}))
}
const handleSubmit = (e) => {
console.log('it got here')
e.preventDefault()
if(title && blurb){
dispatch(ADD_REVIEW({
id: nanoid(),
title,
blurb
}))
// setFormDetails({title: '', blurb: ''})
}
}
return(
<div>
<form onSubmit={handleSubmit}>
<input
type = 'text'
name = 'title'
onChange={handleChange}
/>
<input
type = 'text'
name = 'blurb'
onChange={handleChange}
/>
<button type = "submit">Submit</button>
</form>
</div>
)
}
export default AddReview;
I can notice that you pushing directly to the state, I can suggest to use variable in the state and then modify that variable.
Also I suggest to use concat instead of push. Where push will return the array length, concat will return the new array.
When your code in the reducer will looks like that:
import { createSlice } from "#reduxjs/toolkit";
const initialState = [
reviews: [{id:1, title: 'title 1',blurb: 'blurb 1'},
{id:2, title: 'title 2',blurb: 'blurb 2'}]
]
const venueSlice = createSlice({
name: 'reviews',
initialState,
reducers: {
ADD_REVIEW: (state,action) => {
state.reviews = state.reviews.concat(action.payload);
}
}
})
export const { ADD_REVIEW } = venueSlice.actions
export default venueSlice.reducer
And then your selector will looks like that:
const reviews = useSelector((state) => state.reviews.reviews)
Your code seems to be fine. I don't see any reason why it shouldn't work.
I run your code on stackblitz react template and its working as expected.
Following is the link to the app:
stackblitz react-redux app
Link to the code:
Project files react-redux
if you are still unable to solve the problem, do create the sandbox version of your app with the issue to help further investigate.
Thanks
Expanding on #electroid answer (the solution he provided should fix your issue and here is why):
Redux toolkit docs mention on Rules of Reducers :
They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
and on Reducers and Immutable Updates :
One of the primary rules of Redux is that our reducers are never allowed to mutate the original / current state values!
And as mdn docs specify the push method changes the current array (so it mutates your state). You can read more about mutating the state in the second link link (Reducers and Immutable Updates).
If you really want to keep the state.reviews and avoid state.reviews.reviews you could also do something like this:
ADD_REVIEW: (state,action) => {
state = [...state, action.payload];
}
But I wouldn't recommend something like this in a real app (it is avoided in all the examples you can find online). Some reason for this would be:
It harder to work with, read and track the state when having an overall dynamic state instead of a state structure
It leads to a lot of slices in a real app (creating a slices for an array without grouping the data) which can also become hard to track and maintain.
Usually you need a redux slice in multiple parts of the app (otherwise you can just use state). That data is usually bigger than just an array and not grouping the data properly on reducers can become very very confusing.
But I would definitely advise to use something else (not reviews.reviews). In your case I think something like state.venue.reviews
(so on store.js
...
export const store = configureStore({
reducer:{
venue: reviewReducer // reviewReducer should probably also be renamed to venueSlice or venueReducer
}
})
So an option to avoid state.venue.reviews or state.reviews.reviews would be to export a selector from the venueSlice.js:
export const selectReviews = (state) => state.venue.reviews
and in your Venue.js component you can just use
const reviews = useSelector(selectReviews)
Exporting a selector is actually suggested by the redux toolkit tutorials as well (this link is for typescript but the same applies to javascript). Although this is optional.

Redux Selector not called

I have the following situation:
A selector for a list of countries in my app state
export const getCountries = state => state.countries;
And a selector created using reselect to filter for a country with a specific id
export const getCountryById = countryId =>
createSelector(
[getCountries],
(countries) => {
let countriesList = countries.filter(c => c.id === countryId);
if ( countriesList ) {
return countriesList[0];
}
return null;
}
)
Then a have a country details page component ( acessed through http://localhost:3000/country/4 )
import React from 'react';
import { getCountryById } from '../redux/selectors';
import { connect } from 'react-redux';
const CountryPage = ({country}) => {
return <h1>{country.name}</h1>
}
const mapStateToProps = (state, ownProps) => {
console.log(ownProps.match.params.countryId);
const obj = {
country: getCountryById(ownProps.match.params.countryId)
}
return obj;
};
export default connect(mapStateToProps, null)(CountryPage);
The mapStateToProps is correctly receiving the countryId, but the selector is not being called. What am I missing here ?
Or is it the wrong approach to using the selectors ? I am aware that I can use the full country list from state in the CountryPage and filter it there, but what is the best practice here ?
That's because your getCountryById isn't actually a selector. It's a function that returns a selector.
Given that you are trying to use a unique ID per component, and doing filtering, you probably need to use the "factory function" form of mapState to create a unique selector instance per component instance to get proper memoization behavior.
However, having said that, your selector can also be simplified. You don't need a filter() here, you need a find(), and that means it's also not something that really needs to be memoized. This can be simplified down to:
const getCountryById = createSelector(
[getCountries, (state, id) => id],
(countries, id) => countries.find(c => c.id === id) || null
)
and then used as:
const mapStateToProps = (state, ownProps) => {
return {
country: getCountryById(state, ownProps.match.params.countryId)
}
};

Mapping components to data with nesting

I am looking for a way to handle object data from the server, given data I'd like to pass all the data into a thing, and specify one thing and return that thing, for instance if I have a organization object and I wanted the createdAt date formatted. I would just be able to pass <data.Organization.createdAt {...organization}>. Is there anything that does this?
Here's the data
const contact = {
name: 'Thomas',
uuid: 1234
}
const orgnization = {
contact,
}
const scopedOrgniazation = {
orgnization
}
Here's the Components
import {RF, SalmonLink} from '../index'
const Contact = {
Name: ({ name }) => (name) ? <RF>{name}</RF> : <NA/>,
NameLink: ({ name, uuid }) => (uuid && name) ? <SalmonLink to={`/admin/organizations/${uuid}`}>{name}<SalmonLink> : <NA/>
}
const Organization = {
Contact
}
const ScopedOrganization = {
Organization
}
Desired Components API
I was thinking this may be possible by using something like lodash's flow or flowRight.
<Contact.Name {...contact}>
<Organization.Contact.Name {...organization}>
<ScopedOrganization.Organization.Contact.Name {...scopedOrgniazation}>

Redux as model repository

I'm thinking about using redux as a hard repository for our backend models.
e.g.
endpoints:
/cars
/cars/:id
actions:
listCars
createCar
getCar
deleteCar
updateCar
however i'm not sure how to implement relations properly. Lets say for example calling the api /car/1 gives me the following and i'm maintaining the same structure for tires as i do in the car example given above.
{
id:123,
tires:[41,54,12,10]
}
in this case i would love to eager load the tire id's and map them inside the car reducer. Currently i have a wrapper method which maps data cross reducers but it isn't the prettiest solution. I realise i can also map state in container components but considering there's multiple containers i would like to prevent code replication and work from a single source of truth (redux)
I was wondering if there's a common pattern i could follow for this implementation and if this is a good idea altogether
If your state is:
cars: {
123: {
id: 123,
tires: [41,54,12,10]
}
},
tires: {
41: {
id: 41,
brand: //...
},
54: {
id: 54,
brand: //...
},
// ...
}
Selectors would be:
const getTireIds = (state, { carId }) => state.cars[carId].tires;
const getTire = (state, { id }) => state.tires[id];
Components would look like:
const Car = ({ id }) => <Tires carId={id} />;
const Tires = connect(
// map state to props:
(state, { carId }) => ({
tireIds: getTireIds(state, { carId })
})
)(
// component:
({ tireIds }) => tireIds.map(tireId => <Tire id={tireId}) />)
);
const Tire = connect(
// map state to props:
(state, { id }) => ({
tire: getTire(state, { id })
})
)(
// component:
({ tire: { id, brand } }) => // ...
);
https://vuex-orm.org serves well to implement repository pattern out of box. It's been inspired by https://github.com/redux-orm/redux-orm, yet the latter is way less actively maintained.
Vuexorm is litterally the one package i miss most from vue world when working on react project.

How to use variables in a GraphQL query?

So I have to make two queries. First will return an array with multiple objects and I want to get the ID of the first object to use it in my second query.
The problem is that I can't use b_id: props.getBusiness.business[0]._id
Any idea how can I work with this?
const GET_USER_BUSINESS = gql`
query getUserBusiness {
getUserBusiness {
_id
}
}
`;
const GET_BUSINESS_JOBS = gql`
query getBusinessJobs($b_id: ID!) {
getBusinessJobs(b_id: $b_id ) {
_id
name
}
}
`;
export default compose(
withApollo,
graphql(GET_USER_BUSINESS,
{
name: "business"
}),
graphql(GET_BUSINESS_JOBS,
{
name: "jobs",
options: (props) => (
{
variables:
{
b_id: props.getUserBusiness.business[0].b_id
}
}
)
})
)(ProposalContainer);
Couple of things. One, by default, the graphql HOC passes a prop called data down to the wrapped component with the query data. However, because you specify a name in the HOC's config, the prop will actually be called whatever name you pass it. In other words, you can access the _id this way:
props.business.getUserBusiness[0]._id
...if getUserBusiness returns an array. Otherwise:
props.business.getUserBusiness._id
Secondly, props.business will be undefined until the query completes. We probably don't want to send the GET_BUSINESS_JOBS query until we have an _id to work with. So we want to pass a skip function to the HOC's config:
export default compose(
withApollo,
graphql(GET_USER_BUSINESS,
{
name: "business"
}),
graphql(GET_BUSINESS_JOBS,
{
name: "jobs",
skip: (props) => !props.business || !props.business.getUserBusiness[0]
options: (props) => (
{
variables:
{
b_id: props.business.getUserBusiness[0]._id
}
}
)
})
)(ProposalContainer)

Resources