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.
Related
For example, my app contains the two list: colors & my favorite colors. How to create the re-usable filter-module for this two lists?
The problem is the actions in redux commiting into global scope, so filter-reducer for colors and filter-reducer for favorite colors reacting to the same actions.
I try something like high-order functions that receive the module-name and returned new function (reducer) where classic switch contain module + action.type.
But how make scoped actions or scoped selectors? Best-practise?
Maybe Redux-Toolkit can solve this problem?
High-order reducer
High-order action
High-order selector
Well described here
Name your function like createFilter and add a parameter like domain or scope
after this in switch check it e.g
export const createFilters = (domain, initState) => {
return (state = initState, { type, payload }) => {
switch (type) {
case: `${domain}/${FILTER_ACTIONS.ADD_FILTER}`:
...
and for actions create something like this
const createFilterActions = (domain: string) => {
return {
addFilter: (keys: string[]) => {
return {
type: `${domain}/${FILTER_ACTIONS.ADD_FILTER}`,
payload: keys
}
},
updateFilter: (key: string, state: any) => {
return {
type: `${domain}/${FILTER_ACTIONS.FILTER_UPDATED}`,
payload: { key, state }
}
},
}
}
What I'm trying to achieve:
I have a NextJS + Shopify storefront API application. Initially I set up a Context api for the state management but it's not that efficient because it re-renders everything what's wrapped in it. Thus, I'm moving all state to the Redux Toolkit.
Redux logic is pretty complex and I don't know all the pitfalls yet. But so far I encounter couple problems. For example in my old Context API structure I have couple functions that take a couple arguments:
const removeFromCheckout = async (checkoutId, lineItemIdsToRemove) => {
client.checkout.removeLineItems(checkoutId, lineItemIdsToRemove).then((checkout) => {
setCheckout(checkout);
localStorage.setItem('checkout', checkoutId);
});
}
const updateLineItem = async (item, quantity) => {
const checkoutId = checkout.id;
const lineItemsToUpdate = [
{id: item.id, quantity: parseInt(quantity, 10)}
];
client.checkout.updateLineItems(checkoutId, lineItemsToUpdate).then((checkout) => {
setCheckout(checkout);
});
}
One argument (checkoutId) from the state and another one (lineItemIdsToRemove) extracted through the map() method.
Inside actual component in JSX it looks and evokes like this:
<motion.button
className="underline cursor-pointer font-extralight"
onClick={() => {removeFromCheckout(checkout.id, item.id)}}
>
How can I declare this type of functions inside createSlice({ }) ?
Because the only type of arguments reducers inside createSlice can take are (state, action).
And also is it possible to have several useSelector() calls inside one file?
I have two different 'Slice' files imported to the component:
const {toggle} = useSelector((state) => state.toggle);
const {checkout} = useSelector((state) => state.checkout);
and only the {checkout} gives me this error:
TypeError: Cannot destructure property 'checkout' of 'Object(...)(...)' as it is undefined.
Thank you for you're attention, hope someone can shad the light on this one.
You can use the prepare notation for that:
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Item[],
reducers: {
addTodo: {
reducer: (state, action: PayloadAction<Item>) => {
state.push(action.payload)
},
prepare: (id: number, text: string) => {
return { payload: { id, text } }
},
},
},
})
dispatch(todosSlice.actions.addTodo(5, "test"))
But 99% of the cases you would probably stay with the one-parameter notation and just pass an object as payload, like
dispatch(todosSlice.actions.addTodo({ id: 5, text: "test"}))
as that just works out of the box without the prepare notation and makes your code more readable anyways.
Good afternoon. I am writing an application using react-redux and faced a dilemma. I have already re-thought it several times and can't choose how to organize the project and data structure correctly and conveniently. Design data by inheriting or composing data. I initially went along the path of composition, but I realized that it is inconvenient when there is a one-to-one relationship. I decided to change it to inheritance, because it seemed logical from the point of view of data organization, but there was a big difficulty with reducers, more precisely, I am confused that it turns out to be a single root reducer with a lot of actionTypeskeys . I remember about performance, when elements inherit a data chain from a common ancestor, that this is very bad. And yet I chose this path and I have a question: Please tell me if it is possible to split into several reducers for each level of nesting data. Example
onst initState: IPages = {
idActive: 0,
pages: [
{
id: 1,
title: `Tab #1`,
workspace: {
idActiveDraggableElements: [],
idActiveLines: [],
attributes: {
height: string,
width: string,
viewBox: [0, 0, 5000, 5000]
},
draggableElements: [], // more data
lines: [], // more data
}
},
]
}
Reducer:
export function pagesReducer(
state: IPages = initState,
action: IPageActionTypes
) {
switch (action.type) {
case "ADD_PAGE":
let uniqId = getUniqKeyIdOfArrayList(state.pages);
return {
...state,
pages: state.pages.concat({id:uniqId, title:`Вкладка - ${uniqId}`})
}
case "REMOVE_PAGE": return {
...state,
pages: state.pages.filter(item => item.id !== action.id)
}
case "CHOSE_PAGE": return {
...state,
idActive: action.id
}
case "RENAME_PAGE":
let indexPage = state.pages.findIndex(item => item.id === action.id);
state.pages[indexPage].title = action.title;
return {
...state
}
// ===================
// LONG LIST WHAT BAD...
// It's a bad idea to add editing to the `workspace` field and then `draggableElements`. `lines`
// ... but I understand that this will happen, because I don't know if there is another way.
default:
return state
}
}
Can I edit the `workspace' node without updating the entire application state?
Thanks you for any help.
For data modeling aspect for a 1-to-1 relationship, you can choose either to reference by id or to embed the data. It depends on your query pattern.
In your case which is embedding, you can make use of memoized selectors.
Ideally, since you have an idActive, update your pages data structure to be an object instead of a list.
Like so:
{
pages: {
'1': {
workspace: { ... },
}
}
}
Then for your reducer, think of it as slicing a tree (or nested attribute). Your reducer would then look something like:
function workspaceReducer(state, action) {
// TODO
}
function pagesReducer(state, action) {
switch (action.type) {
case 'UPDATE_WORKSPACE': {
const { id } = action;
const page = Object.assign({}, state.pages[id]);
return {
...state,
pages: {
...state.pages,
[id]: {
...page,
workspace: workspaceReducer(page.workspace, action)
}
}
}
}
}
}
Then to prevent unnecessary re-renders, using memoized selectors,
it would be like:
import { createSelector } from 'reselect';
const pages = state => state.pages;
const activePage = state => state.idActive;
const getActivePage = createSelector(
activePage,
pages,
(id, pages) => pages[id]
);
const getWorkspace = createSelector(
getActivePage,
page => page.workspace
);
I'm trying to access a Container's props (that were passed in from Redux state) from within mapDispatchToProps (mDTP). My container is responsible for dispatching an action to fetch data, but the action that it dispatches needs access to a piece of redux state. That state will be used as a field in the HTTP header.
The redux state that I am trying to access is languages. I want to then pass it in as a header to mDTP
Here is my container:
const ExperienceListContainer = ({ list, loading, failed, readList, languages }) => {
useEffect(() => {
readList()
}, [])
const apiRequestParams = {
endpoint: 'GetToursByCity',
count: 10,
step: 10,
cityid: 'e2e12bfe-15ae-4382-87f4-88f2b8afd27a',
startdate: '2018-12-20 8:30:00',
enddate: '2018-12-20 22:15:00',
sort_by: 'rating',
guests: 10,
min_price: 1,
max_price: 100.50,
time_of_day: 1,
sort_by: 'rating',
categories: '',
languages: languages
}
return <ExperienceList {...{ list, loading, failed, languages }} />
}
ExperienceListContainer.propTypes = {
list: T.arrayOf(T.object).isRequired,
limit: T.number,
loading: T.bool,
failed: T.bool,
readList: T.func.isRequired,
filter: T.shape({
interests: T.arrayOf(T.shape({
id: T.number.isRequired,
text: T.string.isRequired
})),
languages: T.arrayOf(T.shape({
id: T.number.isRequired,
text: T.string.isRequired
})),
}),
}
const mapStateToProps = state => {
return (
({
list: state.resource.experiences,
languages: state.filter.languages
})
)
}
const mapDispatchToProps = (dispatch) => ({
readList: () => {
return (
dispatch(resourceListReadRequest('experiences', { apiRequestParams }))
)
}
})
export default connect(mapStateToProps, mapDispatchToProps)(ExperienceListContainer)
The problem with this current code is that mDTP cannot access apiRequestParams. It is undefined as far as it is concerned.
Based on my research, the answers that pop up are to use mergeProps (as an arg passed into connect()) and ownProps (a param included with mapDispatchToProps), but neither of these seem to be working, at least in the way I'm implementing them. Is there a better way to accomplish what I'm trying to do?
You have to pass them to readList as a parameter to access them like this:
readList(apiParams)
And use them like this:
readList: apiRequestParams => {
return (
dispatch(resourceListReadRequest('experiences', {
apiRequestParams }))
)
This is because apiRequestParams is local to the container itself and cannot be access outside of the component.
You would also have to declare it above the useEffect and pass it as parameter to the useEffect so that it gets evaluates, if the language changes for example and keep the reference with useRef or declare it within the useEffect so that you don't cause an infinity loop.
Hope that helps. Happy coding.
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.