Is it possible to create a React Hook factory function? - reactjs

I have a React app that relies heavily on redux, react-redux, and redux-saga. I'm starting to experiment with React Hooks, and the useSelector and useDispatch hooks in react-redux, and I've run into an issue with how to essentially write a factory function to generate hooks for each of my redux nodes.
In general, I have a redux node for each API endpoint my app consumes. There are about 100 unique endpoints in this app, and so there are about 100 redux nodes. Each of those nodes then corresponds to one state-[node].js file, like state-users.js, etc. Those state files each encapsulate the endpoint they should call, trigger sagas to handle the HTTP lifecycle (start, fail, success, etc), and so on.
Over time, I've written code that abstracts much of the boilerplate away into utility functions, including functions that auto generate action creators, selectors, reducers, and the connect function. It's a bunch of code, and somewhat convoluted, but the gist looks something like this. First, I set up an array of objects describing actions that are possible for this redux node. A simplified version looks like this:
const messages = [
{ action: 'GET', creator: 'get', connect: true },
{ action: 'POST', creator: 'post', connect: true },
{ action: 'CLEAR', creator: 'clear', connect: true },
];
This describes that there will be three actions, get , post, and clear, and that they should be exposed in the connector. I have a set of common reducers (e.g. most get reducers are identical across nodes), so those are assumed here based on name.
Then I set up a list of selectors, like this:
const selectorKeys = ['data','pending','errors'];
...and then I have a factory function that I feed these arrays into, which looks something like this:
const { connector } = stateGenerators({
keyword: 'users', //redux node keyword
messages: messages,
selectorKeys: selectorKeys
})
This is a simplified version of how everything really works, but it's the crux of it. Again, all of the above code is abstracted into a state file, like state-users.js.
Then, in my class component, I just need to import the connector from state-users.js, like this:
import { connector } from 'state-users';
class Users extends Component {
componentDidMount() {
this.props.get();
}
componentWillUnmount() {
this.props.clear();
}
render () {
const { data } = this.props;
return (
<div>
{data.map()}
</div>
)
}
}
export connector()(Users)
This model does get clunky at times, but the nice part is that nearly all of the redux boilerplate is abstracted into common files, so my individual state files are, for the most part, really simple.
Now, to the question: is it possible to do something like this "factory function" approach with Hooks? So far in my experimenting I have not figured out how to do this. There are two fundamental issues:
First, you can't put hooks in loops, so I can't do this:
const selectors = {}
const reduxNodeKeyword = 'users';
['data','pending','errors'].map((selectorKey) => {
selectors[selectorKey] = useSelector((state) => state[keyword].selectorKey);
})
That code results in this error:
React hook "useSelector" cannot be called inside of a callback.
In practice, that means I can't just pass in an array of selector keys I'd like and then have it spit back my selectors.
Second, you can't put hooks inside conditionals. So since the first idea failed, I tried a different approach in my factory function, which looks something like this:
if (_.includes(stateSelectors, 'data')) {
result['data'] = useSelector((state) => state[keyword].data);
}
That results in this error:
React hook "useSelector" is called conditionally. React Hooks must be called in the exact same order in every component render
So that's also a bummer. But I think what I'm left with is that for each of my 100 Redux nodes, I would have to write a custom, and verbose, hook to more or less replicate connect.
I know, I know, hooks should encourage me to think differently, but I still need to fetch data from the server and provide it to components, and I need to follow that same basic pattern 100 times. Even if hooks makes my components more elegant (as I expect it would), the thought of writing 100 or so hooks out by hand, each with a fair amount of repeated data, rather than somehow auto-creating them using some sort of factory approach, gives me hives.
Help?
Thanks!

Not sure if this will be useful for others, but I found an approach that works for me. I can't put hooks in iterators, nor in if statements, but I do have common patterns all over the place. So the answer was to abstract those common patterns into generic hooks. For example, I have a hook called useCommonReduxListSelectors, which looks like this:
export const useReduxListCommonSelectors = (keyword: string) => {
return {
data: useSelector((state: any) => state[keyword].data),
errorMessage: useSelector((state: any) => state[keyword].errorMessage),
filters: useSelector((state: any) => state[keyword].filters),
pending: useSelector((state: any) => state[keyword].pending),
totalCount: useSelector((state: any) => state[keyword].totalCount)
};
};
I have lots of Redux state nodes that are responsible for handling lists returned from an API, and the data shape of most of those list endpoints is what you see above. So then the hook that a component would invoke uses useReduxListCommonSelectors, like this:
export const useReduxState = ({ id, defaultValues }) => {
const selectors = useReduxListCommonSelectors({
keyword:'users'
});
return {
...selectors,
};
};
And then obviously useReduxState can have any other data in there (actionCreators, custom selectors, etc) that is required by that node. So it allows me to abstract the most common patterns into reusable hooks, and also have the flexibility to add custom code to each useReduxState instance as needed.

Related

Managing a collection of different views with redux toolkit

I'm trying to manage the state of an app that uses react-mosaic with redux.
The app can have many views of different types. Each view does it's own thing, but has some "layout" specific operations, like "remove", "copy" etc.
So in addition to something like copyLayout:PayloadAction<{layout}> i ended up with a bunch of actions that look like someAction:PayloadAction<{modulePath,foo,bar}>, which i dispatch as such:
const dispatch = useAppDispatch()
const {modulePath} = useModuleContext()
...
dispatch(someModuleAction({modulePath,foo,bar})
I would like to avoid having to be explicit about a "path" where this state lives and have something like:
const {dispatchWithPath} = useModuleContext()
...
dispatchWithPath(someAction({foo,bar})
Right now im trying to strip my action creators i get from the slice with PayloadAction<{modulePath, ...}> :
const createActionModuleWrapper = <
T extends ActionCreatorWithPayload<$IntentionalAny>
>(
action: T
) => (modulePath: ModulePath) => {
type PayloadWithPath = Parameters<T>[0] & { modulePath: ModulePath }
type PayloadWithoutPath = Omit<PayloadWithPath, 'modulePath'>
const wrapped = (payload: PayloadWithoutPath) => {
const paramsWithPath: PayloadWithPath = { ...payload, modulePath }
action(paramsWithPath)
}
return wrapped
}
With the idea to stick an actions object in this context where i would be explicit:
const moduleActions = {
someAction:createActionModuleWrapper(slice.actions.someAction),
...
}
const useSomeContext = ()=>{
const {modulePath} = useModuleContext()
const actions = {
someAction: moduleActions.someAction(modulePath)
}
}
But it feels like a hefty amount of boiler plate. I would also have to repeat these universal actions such as "copy", "remove", "duplicate" wherever i mention a "do something specific in THIS module".
I transform these creators in the slice file, so maybe just a universal useModuleDispatch without these specific actions per context would do the trick.
Does any of this feel right? Is there a better way to manage collections such as this? I cant figure out a way to setup a slice with toolkit and break this up. Each someModuleAction:
someModuleAction:(state, action:ModuleAction)=>{
if(getModule(state, action.payload.modulePath).type !==SomeModuleType) return
//SomeModule domain
}
I saw a mention of a different pattern where each action has a metadata field. Would a universal dispatch method work better with this? Now that i think of it, if i used a better PayloadAction (i use ModuleAction<T> to `T & {modulePath:ModulePath}, but only on some actions) i could just ignore the known, maybe empty field if i'm in a reducer that doesn't care about it.
Roughly the pattern that i'm replacing:
const {changeModule} = useModuleContext(SomeType)
const setFooInline = ()=>
changeModule((oldModule)=({...oldModulue, foo})
const setBarInline = ()=>
changeModule((oldModule)=({...oldModulue, bar})
So it's super powerful, since i get the one and only right setter, but then i have a million of these inline setters/action creators, and a ton of prop drilling to get them into places, since they all operate on this one generic update state tree root, while having access to the entire state of that tree, which is what i'm trying to avoid.

Is it bad practice to select an entire slice in redux?

I want to select an entire slice in redux like the following:
interface AuthState {
user?: User;
shouldShowLogin: boolean;
}
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setUser: ...,
showLogin: ...,
hideLogin: ...,
},
});
export const useAuth = () => useSelector((state) => state.auth);
So I could do something like const { user, shouldShowLogin } = useAuth(); within components. This is handier in most cases but I'm not sure if it's bad practice/inefficient.
I want to do something similar for dispatch (i.e. abstract away useDispatch()). Maybe something like const setUserWithDispatch = (info) => useDispatch()(info); so I can do simply setUserWithDispatch(userInfo) in my components, without having to do useDispatch() and then dispatch(setUser(userInfo)).
Ideally, I'd like to do const { user, shouldShowLogin, setUser, showLogin, hideLogin } = useAuth(); but I'm not sure how to go about it. Again, is this bad practice/inefficient? Why doesn't redux configure this automatically?
The component will rerender when the value selected from useSelector changes. Consequently, you generally want to select the minimum amount of information that you need in order to minimize rerenders.
If there are multiple properties in state.auth and you only care about one then the downside to this destructuring approach is that it will cause the component to rerender when other properties of the object change. In your example you seem to be using all of the properties so it shouldn’t matter.
I think you will have a hard time doing something custom with dispatch without violating the rules of hooks. Hooks cannot be called conditionally and must be called at the top level of the component. You cannot call useDispatch (or a custom hook that uses it) inside of an event handler callback, inside a useEffect function, etc.
The current implementation of the useDispatch hook means that you call useDispatch once per component to get access to the dispatch function for the current store based on the react-redux context. The returned dispatch function is a pure function (not a hook) and can be used anywhere.
You could use redux’s bindActionCreators to bind actions to the dispatch for a particular store instance. This sounds like what you are trying to do, but it is advised against. The downside is that you create an unnecessary coupling between the actions and a particular store instance. You would lose the ability to use a different store instance for testing.

What is the meaning of actions and action types in React-redux?

Now if i want to change value in store i should do following steps:
Go to constants/actionTypes file, create a line with action type
Go to actions and create action function
In each component where i use it i should create a function for mapDispatchToProps
In reducer i should write a logic of changing
Whats the point of such complexity?
Will it be wrong if i will do just one file with actions which will change the state? For example:
// actions.js
export const setCategories = (payload, setState, currentState) => setState({ categories: payload })
export const addCategory = (payload, setState, currentState) => setState({ categories: [...currentState.category, payload] })
To make it work i can create just couple of universal functions for all projects
1) getActions, which authomaticly collects all exports from actions.js and provide them to mapDispatchToProps, so in all components we could just write
const mapDispatchToProps = getActions
code of it can be something like
// actionsDispatcher.js
import * as actions from 'actions'
const getActions = (dispatch, ownProps) => {
Object.keys(actions).forEach(actionName => {
const fn = actions[actionName]
actions[actionName] = payload => dispatch({ action: fn, payload, type: _.toSnakeCase(actionName) })
}
return actions
}
which means we pass to dispatch the function of action from actions.js
2) setState which will work similary to react function, but for redux state
then in reducer function we just right
function rootReducer(state = initialState, action) {
if (action.action) {
action.action(action.payload, setState, state)
}
// here we make it extandable for ordinary way if required
if (action.type === '...') {
// ...
}
}
and nothing else...
So the question is whats wrong in such approach that will require for coder just write a function in one file 'actions.js' and call it from any component as props.someActionName(someParams) instead of changing 4 differents files?
Thank you
Redux is supposed to make complex requirements easier to implement but if you have simple requirements then it makes implementing these requirements more complicated.
The motivation mentions CQRS(Command Query Responsibility Segregation) that separates how you read from store (in redux with selectors and I'm a big fan of reselect) with how you write to it (with action and reducers).
The actions and reducers are the command (write) part of CQRS and is event sourcing, redux is sometimes referred to as an event store. This enables you to add or remove handlers (reducers or middle ware) for your events (actions) that can update the store, dispatch other events (=actions), do asynchronous stuff, write to local storage.
If you need to do all these things in one function (async fetch, write to local storage, call other functions (dispatch other actions),...) then that function becomes unmanageable.
Even if the function only calls other functions then it still needs to know the entire process of certain action. But if (for example) you had a local storage middleware that would write to storage on certain actions then no other code needs to know how or when it's called. So when logic of writing to local storage changes it is limited to the local storage middle ware.
This is the advantage of handlers (reducers, middleware) listening to events (actions), the handler only needs to know about a small portion of the process, not the entire process.
With event resourcing we also know why the state has a certain value instead of only knowing what the state is, the article states:
However there are times when we don't just want to see where we are, we also want to know how we got there.
Another big advantage of an event store is that you can re create the data by playing back the events. All this is excellently done with redux def tools.
Here is a great book on React with Redux.
Conventional-redux is a:
Library for small and medium react applications, it wraps the react-redux and provides API based on convention over configuration pattern without breaking redux compatibility.
You simply define an interactor:
class CounterInteractor {
// initial state
defaultState() {
return 0;
}
// actions:
doubleAsync() {
setTimeout(() => { this.dispatch('counter:double') }, 500)
}
// reduce methods:
onIncrement() {
return this.state + 1;
}
onDouble() {
return this.state * 2;
}
}
And dispatch actions to that interactor from your connected component component. That's it!

Better way to structure react app?

I started my app without any state management dependencies and my app's state looked something like this:
state = {
...initState,
}
///some actions I need to perform
handleChange = (e) => {
this.setState(setDefaultValues(e));
this.setState(setBmr);
this.setState(setTdee);
this.setState(setTrainingAndRestDaysCal);
this.setState(setTrainingMacros);
this.setState(setRestMacros);
}
here I import my initState from separate file (to save some space). Then I have handleChange where I'm passing functions to multiple this.setState because my next state data depends on previous. I'm importing those functions from separate file (to save some space as well)
Then I came to the point where I realized I'm passing props all over the place so I introduced the new react's context API. Which works very well in my opinion. Kind of like a redux just without a big boilerplate. So this context API helped me with that prop drilling through the child components. To use the context API i had to do some changes to my state object, so it looks like this now:
state = {
...initState,
handleChange: (e) => {
this.setState(setDefaultValues(e));
this.setState(setBmr);
this.setState(setTdee);
this.setState(setTrainingAndRestDaysCal);
this.setState(setTrainingMacros);
this.setState(setRestMacros);
},
setTrainingTrueOrFalse: (isIt) => {
this.setState({ trainingToday: !isIt })
},
saveNewMeal: (meal) => {
const meals = this.state.meals
this.setState({
meals: { ...meals, meal }
})
}
Here I added my handleChange to my state so I can pass it via context api. Then I have created some new functions on the state an realized my state now is getting too messy.
I have a function on the state (handleChange ) that uses other functions imported from setStateFunctions.js
I have functions where logic is not extracted to setStateFunctions.js
On a high level my app has 2 main components: Calculator & MealPlanner
Calculator - calculates the macros and calories and passes the result
to MealPlanner
MealPlanner - uses data from calculator and creates meals
==================================================
What's the better approach to structure my app's state and functions?
Do I really need to introduce redux?
What would be my reducers structure?
How would this look like using just react's new context API?
Your app is sized right to go without adding redux for state management.
1) What's the better approach to structure my app's state and functions?
Let a top-level app maintain state that includes "handler" functions.
2) Do I really need to introduce redux?
Not needed. Keep your props organized and let the App handle the "events".
3) What would be my reducers structure?
Avoid it and go vanilla react.
4) How would this look like using just react's new context API?
Context feels overkill and possibly entangling (two components could drift on the understanding of how to use what is exposed as shared, global state in the context).
Let your composing App manage the state and pass props to the child components (Calculator and MealPlanner). You want two-way communication between those, so also pass "handling" functions that change the state within App to get the effect to ripple to the other via passed-in props. Something like the following:
class App extends React.Component {
state = {
...initState, // not sure if this is global or what, but assuming defined
handleCalculation: () => {
// do some state changes on this ala this.setState()
},
handlePlanning: () => {
},
};
render() {
return (
<div>
<MealPlanner {...this.state} />
<Calculator {...this.state} />
</div>
);
}
}
Let MealPlanner and Calculator set required propTypes accordingly.

Using Redux with models

I've started using Redux with React and I absolutely love it. However, the problem I'm having currently is that besides state, I also have more information I need to store/use throughout my application.
In this specific case I have a model with state that's fetched from an API. This model also has some info about itself, e.g. how you display a property on the screen "name" => "Name of the blabla". I understand how to work with state using Redux, but I'm have trouble seeing what do with this other info that I still need propagated throughout the application but is not actually state.
According to Redux, the State is the only "source of truth". And it should not have duplication (which would lead to inconsistencies).
So your state should store the name, but not the computed label property.
Indeed, "Name of the blabla" is a function (in the mathematical sense) of your Name value, and if they differ (for example, if at some point name === 'foo' but the label is 'Name of the bar' instead of 'Name of the foo'), then you have a problem...
So what I would do, is just store the minimum in your state (name in that case), and compute the label directly in the Component, where you need it.
If you need that to be re-used, then create a Component that only does take your name as a prop, and render a string with "Name of the blablaba" (if name = blabla I suppose).
If you need more complex computation (say you have multiple labels, date calculations etc.), you can always create a function that takes your State in input, and spit out your "Model" in output with everything calculated.
Redux is very functional in nature, so you might as well embrace it :)
I know I'm kind of late to the party but I thought someone might use the answer. What has work for me this far after working with React for some years now is to have a structure that is sort of like this:
State: which sets the structure (or 'schemas') of my data.
Actions: make changes to this state. These actions can handle sync or async operations.
Sagas: they handle async actions.
Selectors: they handle the structure of the data that I need for the view/for the API.
Constants: other data that won't change through time and that makes no sense to add to my state.
So having said that the flow of my apps is something like this:
An ACTION is dispatched => If that action is async a SAGA is listening and it performs the fetch operation => This saga saves updates to the STATE => [React components layer from now on] => If my view needs the data from my state in a different format for whatever reason I send it through a SELECTOR which will change that format => Then I attach this new parsed data to my container component.
An other flow could be one in which you need static data that is not in your state. In that cause I would save it in an object in a separate file and would import it to my container component directly (I never import anything in my children/presentational components directly. Only other components. The data is handled in a separate layer than the components).
A third kind of flow I can think of right now is when you need to make a POST to your API and for whatever reason the data in your state needs some parsing before doing so. In that case I would do the same that in the first example but the other way around: dispatch an ACTION => that is handled by a SAGA => before doing the fetch I would bring my data already structured for my POST (sagas has a method called select to help you use selectors here) => then I would perform the async operation => update the state accordingly.
Just in case you don't know what I mean by selectors or sagas some links here:
Sagas: https://github.com/yelouafi/redux-saga
Selectors: https://github.com/reactjs/reselect
I think models are as necessary for a Redux based app as for any other system.
Models are the vocabulary of a system. Models bring sanity to the codebase. Without them a codebase looks like a series of insane distorted thoughts.
You can use state functions to fill in the need of models in ReactJS+Redux apps.
Just like models hold data and methods, these objects hold only the functions that can be applied to state.
Read here : https://medium.com/#nshnt/state-functions-for-modeling-with-redux-a9b9d452a631.
Here is the famous Redux TODO app example with state functions :
todo_reducer.js :
import TODO from './todo_state';
const todoListReducer = (state=TODO.initialState(), action)=>{
switch(action.type){
case 'ADD_TODO' :
return TODO.addTask(state, action.taskName);
case 'FINISHED_TODO':
return TODO.setFinished(state, action.taskID );
case 'PENDING_TODO':
return TODO.setPending(state, action.taskID );
default :
return state;
}
};
export default todoListReducer;
todo-state.js :
export default {
initialState: () => [],
addTask: (todoList, name)=> todoList.concat({id: todoList.length, name: name}),
setFinished: (todoList, taskId) => (
todoList.map(task=> task.id === taskId ? {...task, complete: true} : task)
),
setPending: (todoList, taskId) => (
todoList.map(task=> task.id === taskId ? {...task, complete: false} : task)
),
pending: todoList=> todoList.filter(task=> !task.complete)
};
I also use these state functions in component, if component need some manipulation of state.
todo_list.js :
import React from 'react';
import {connect} from 'react-redux';
import TODO from './todo_state';
const TodoList = ({tasks, showCompletedTasks, toggleTodo})=> {
const toListElement = (task) => (
<li key={task.id}>
<input type="checkbox" checked={task.complete} onChange={(e)=> toggleTodo(task)}/>
<label>{task.name} {task.complete ? "Complete" : "Pending"}</label>
</li>
);
const visibleTaskList =
(showCompletedTasks ? tasks
: TODO.pending(tasks)).map(toListElement);
return (
<ul className="todo-list">
{visibleTaskList}
</ul>
);
}
.....
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
Use Reselect
Reselect is a simple library that sits in your app. It's primary function is to aggregate data from your redux store.
Create a reselect function
Taken from https://medium.com/#parkerdan/react-reselect-and-redux-b34017f8194c
import { createSelector } from 'reselect'
// selector
const getBar = (state) => state.foo.bar
// reselect function
export const getBarState = createSelector(
[ getBar ],
(bar) => bar
)
The idea is that you connect your component with redux-connect or map state to props but instead of using the store directly you pass the store to a selector. This selector will have a function that lets you aggregate data or transform it any way you like.
import React from 'react'
import { connect } from 'react-redux'
import { getBarState } from '../selectors'
const mapStateToProps = (state) => {
return {
bar: getBarState(state)
}
}
The advantage of this approach is that you can reuse a selector on any component quite easily. You manipulate your data before it ever reaches a component (Separation of concerns principal). This gives you 2 big advantages.
Firstly, your redux store can remain unpolluted with duplicate or calculated data.
Secondly, your components can be built to expect data structures that immediately make sense to them.
Conclusion
Reselect helps your React apps become more structured.

Resources