Alternative to Redux reducer - javascript

I'm building a library that other apps will use. My library is an extension to redux.
To keep this question as general as possible, I currently have middlewares, action creators and one reducer.
The reducer is the problem because my reducer depends on the state structure which I, as a library developer, can't and shouldn't know. The user may use a combineReducers function or other function and give my reducer's state any name he wants.
My question is - What options Redux.js library provide to library developers in a case like this; hiding reducers/other alternatives to reducers?

Wrap your library in a configuration function, which requires the user to supply a selector, that points to place in the state that your reducer occupies.
In addition, if users access your state (not the case for you right now), you can supply selectors to use the state, without knowing it's structure.
A general non-working example:
const createSelectors = (mainSelector) => ({
selectorA: (state) => mainSelector(state).dataA,
selectorB: (state) => mainSelector(state).dataB,
});
const createMiddlewares = (actionTypes, selectors) => {
const middlewareA = ({ getState, dispatch }) =>
next => action => {
const myStateA = selectors.selectorA(getState());
};
return {
middlewareA
};
};
const factory = (mainSelector = ({ myState }) => myState) => {
const actionTypes = {};
const actions = {};
const reducer = () => {};
const selectors = createSelectors(mainSelector);
const middleware = createMiddlewares(actionTypes, selectors);
return {
actionTypes,
actions,
reducer,
middleware,
selectors
};
};

This package minimize needs for writing reducers, because it is using auto merge feature with actions. There is some solution with arrays too.
https://www.npmjs.com/package/micro-reducers

I think instead of struggling with the complex Redux library. you should give it a try with https://rootzjs.org/ an alternative to redux. Its literally like a cakewalk.

Related

React Hooks - Global state without context

I am implementing a hook "useUserPosts", which is supposed to be used in several routes of my application.
As I already have a context "PostsContext" which re-renders my Cards when data changes (totalLikes, totalComments, descriptions, ...), I have decided to avoid creating another one called "UserPostsContext" which purpose is to return the user posts array.
I know, why not to use the PostsContext instead?...
The answer is that, in PostsContext, to avoid performance issues, I am storing a map (key, value), in order to get/update the posts dynamic data in O(1), something which is only useful in my components (so, it is used to synchronize Cards basically)
Is it possible/a common practice in React to create hooks that handles global states without using the Context API or Redux?
I mean, something like
// Global State Hook
const useUserPosts = (() => {
const [posts, setPosts] = useState({});
return ((userId) => [posts[id] ?? [], setPosts]);
})();
// Using the Global State Hook
function useFetchUserPosts(userId) {
const [posts, setPosts] = useUserPosts(userId);
const [loading, setLoading] = useState(!posts.length);
const [error, setError] = useState(undefined);
const cursor = useRef(new Date());
const hasMoreToLoad = useRef(false);
const isFirstFetch = useRef(true);
const getUserPosts = async () => {
// ...
}
return { posts, loading, error, getUserPosts };
}
Note: my purpose with this is to:
1. Reproduce some kind of cache
2. Synchronize the fetched data of each stack screen that is mounted in order to reduce backend costs
3. Synchronize user posts deletions
Even if I think creating a new global state is the best solution, if you really want to avoid it, you could create your own as follow :
export class AppState {
private static _instance: AppState;
public state = new BehaviorSubject<AppStateType>({});
/**
* Set app state without erasing the previous values (values not available in the newState param)
* */
public setAppState = (newState: AppStateType) => {
this.state.next({ ...this.state, ...newState });
};
private constructor() {}
public static getInstance(): AppState {
if (!AppState._instance) {
AppState._instance = new AppState();
}
return AppState._instance;
}
}
With this kind of type :
export type AppStateType = {
username?: string;
isThingOk?: boolean;
arrayOfThing?: Array<MyType>;
...
}
And use it this way :
const appState = AppState.getInstance();
...
...
appState.setAppState({ isThingOk: data });
...
...
appState.state.subscribe((state: AppStateType) => {// do your thing here});
Not sure this is the best way to create a state of your own but it works pretty well. Feel free to adapt it to your needs.
I can recommand you to use some light state management library as zustand: https://github.com/pmndrs/zustand.
You can with this library avoid re-render and specify to re-render just want the data you want change or change in certain way with some function to compare old and new value.

React - Running side effects inside pure functions

Introduction
My current use case requires to store the most fresh state updates in a cache. As state updates are async, and there can be a lot of components updating the same one in parallel, it might be a good option to store them inside the body of the useState or useReducer pure functions.
But... side effects come, and the frustration start. I have tried to await dispatches, creating custom hooks "useReducerWithCallback", and other stuff, but I don't see the correct solution to my problem.
Problem
I have a module usersCache.js which provides me with the necessary methods to make modifications to my cache:
const cache = {};
export const insert = (id, data) => ...
export const get = (id) => ...
// and more stuff
I am trying to update this cache when I make state updates. For example:
const currentUser = useContext(CurrentUserContext);
...
// Note: setData is just the state setter useState hook
currentUser.setData((prevData) => {
const newTotalFollowing = prevData.totalFollowing + 1;
usersCache.update(currentUser.data.id, { newTotalFollowing }); <---- SIDE EFFECT
return { ...prevData, totalFollowing: newTotalFollowing };
});
And same stuff in my otherUsers reducer
import { usersCache } from "../../services/firebase/api/users"
export default (otherUsers, action) => {
switch (action.type) {
case "follow-user": {
const { userId, isFollowing } = action;
const prevUserData = otherUsers.get(userId);
const newTotalFollowers = prevUserData.totalFollowers + (isFollowing ? 1 : -1);
usersCache.update(userId, { totalFollowers: newTotalFollowers }); // merge update
return new Map([
...otherUsers,
[
userId,
{
...prevUserData,
totalFollowers: newTotalFollowers
]
]
);
}
...
}
}
As in pure functions we shouldn't perform side effects... Is there any other approach to handle this?
Note: I am not using Redux
You can check this full working example using the repository pattern and react hooks to simplify async actions with state dispatches. I know you are not using redux but you can adapt this example using the useReducer hook to connect it to your React Context store.

What's the point of Actions and Reducers in Redux?

I have been using Redux for a while, but I still can't see the point of actions and reducers.
As described in the docs, a reducer can be summarised as (previousState, action) => newState. The same principle applies for React's useReducer.
So this reducer function basically handles all actions, which seems like a violation of the Single Responsibility Principle. I'm sure that there's a good reason to do it this way, but I don't see it.
It would make more sense to me to just have a function per action. So instead of having an ADD_TODO action you would have a addTodo(previousState, todoText) => newState function. This would reduce (no pun intended) a lot of boilerplate code and might even give a slight performance improvement as you no longer need to switch through the action types.
So my question is: What's the advantage of having a reducer as opposed to a single-action function?
So if your question is why do we use reducers at all?
The real boilerplate of Flux is conceptual: the need to emit an
update, the need to register the Store with a Dispatcher, the need for
the Store to be an object (and the complications that arise when you
want a universal app).
That is a fundamental design choice redux has made as it is inspired from flux.
If you do not like the switch cases, and there by the reducer size. You can have something like this below:
export const todos = createReducer([], {
[ActionTypes.ADD_TODO]: (state, action) => {
const text = action.text.trim()
return [...state, text]
}
})
Above is a function that lets us express reducers as an object mapping from action types to handlers.
createReducer can be defined as :
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
You can read more on this here
Actually it is a silly design and makes no sense.
It is just a kind of over-engineering. Because in most case, you could have just used a simpler function to update a state instead of doing it a silly way by making it two functions (reducer and action).
By adopting redux in your project, you may write tons of reducer and action codes which looks extremely ugly and difficult to do code reviews
Try to imagine, if useState function is design by redux way!
const [todos, setTodos] = useState([])
this one line code now would looks like this(tons of ugly codes):
export interface TODO {
name: string
}
export interface ITodosAction {
type:
| "SET_TODOS";
payload: {
todos?: TODO[];
};
}
export default function todosReducer(
state: TODO[] = [],
action: ITodosAction
): TODO[] {
export function setTodos(todos: TODO[]): ITodosAction {
return {
type: "SET_TODOS",
payload: { todos }
};
}
const todos = useSelector(state=>state.todos)
dispatch(setTodos(["A", "B"]));
if useState became like this, we would have to suffer using react (thanks to react's team for not adopting an over-engineering and silly design like redux).
Remember: never try to figure out why this design is good when it has already brought you trouble. We use redux not because redux is good (instead it is a worst design i've ever seen) but because we don't have many choices
I think you're not appreciating how flexible reducers are, you can create as many of them as you like:
const initialState = {
key: 'someValue',
key2: 'someValue',
key3: {
key31: 'someValue',
key32: 'someValue',
key33: []
}
}
// all the below reducers can live in separate files and only handle a single action, if they want, no switches involved at all
const key = (state = initialState.key, action) => { /* the logic to be performed at 'key' */ }
const key2 = (state = initialState.key, action) => { /* the logic to be performed at 'key2' */ }
const key31 = (state = initialState.key3.key31, action) => { /* the logic to be performed at 'key3.key31' */ }
const key32 = (state = initialState.key3.key32, action) => { /* the logic to be performed at 'key3.key32' */ }
const key33 = (state = initialState.key3.key33, action) => { /* the logic to be performed at 'key3.key33' */ }
const key3 = combineReducers({key31,key32,key33})
// this is the top level reducer for your store
const myTopLevelReducer = combineReducers({key,key2,key3});

Passing multiple props and actions in react / redux

I'm using React and Redux in my web app.
In the login page, I have multiple fields (inputs).
The login page in composed from multiple components to pass the props to.
I was wondering how should I pass the props and update actions.
For example, lets assume I have 5 inputs in my login page.
LoginPage (container) -> AuthenticationForm (Component) -> SignupForm (Component)
In the LoginPage I map the state and dispatch to props,
and I see 2 options here:
mapStateToProps = (state) => ({
input1: state.input1,
...
input5: state.input5
})
mapDispatchToProps = (dispatch) => ({
changeInput1: (ev) => dispatch(updateInput1(ev.target.value))
...
changeInput5: (ev) => dispatch(updateInput5(ev.target.value))
})
In this solution, I need to pass a lot of props down the path (the dispatch actions and the state data).
Another way to do it is like this:
mapStateToProps = (state) => ({
values: {input1: state.input1, ..., input5: state.input5}
})
mapDispatchToProps = (dispatch) => ({
update: (name) => (ev) => dispatch(update(name, ev.target.value))
})
In this solution, I have to keep track and send the input name I want to update.
How should I engage this problem?
It seems like fundamental question, since a lot of forms have to handle it,
but I couldn't decide yet what would suit me now and for the long run.
What are the best practices?
I think best practice would be to handle all of this logic in the React component itself. You can use component's state to store input's data and use class methods to handle it. There is good explanation in React docs https://reactjs.org/docs/forms.html
You probably should pass data in Redux on submit. Ether storing whole state of the form as an object, or not store at all and just dispatching action with api call.
TL;DR. it's a more 'general' coding practice. But let's put it under a react-redux context.
Say if you go with your first approach, then you will probably have 5 actionCreators as:
function updateInput1({value}) { return {type: 'UPDATE_INPUT1', payload: {value}} }
...
function updateInput5({value}) { return {type: 'UPDATE_INPUT5', payload: {value}} }
Also if you have actionTypes, then:
const UPDATE_INPUT1 = 'UPDATE_INPUT1'
...
const UPDATE_INPUT5 = 'UPDATE_INPUT5'
The reducer will probably look like:
function handleInputUpdate(state = {}, {type, payload: {value}}) {
switch (type) {
case UPDATE_INPUT1: return {..., input1: value}
...
case UPDATE_INPUT5: return {..., input5: value}
default: return state
}
}
What's the problem? I don't think you're spreading too many props in mapStateToProps/mapDispatchToProps, Don't repeat yourself!
So naturally, you want a more generic function to avoid that:
const UPDATE_INPUT = 'UPDATE_INPUT'
function updateInput({name, value}) { return {type: UPDATE_INPUT, payload: {name, value}} }
function handleInputUpdate(state = {inputs: null}, {type, payload: {name, value}}) {
switch (type) {
case UPDATE_INPUT: return {inputs: {...state.inputs, [name]: value}}
default: return state
}
}
Finally, the "selector" part, based upon how the state was designed, get component's props from it would be fairly trivial:
function mapStateToProps(state) { return {inputs: state.inputs} }
function mapDispatchToProps(dispatch) { return {update(name, value) { dispatch(updateInput(name, value)) } }
In summary, it's not necessarily a redux/react problem, it's more how you design app state, redux just offers you utilities and poses some constraints to enable "time traveling" (state transitions are made explicit within a mutation handler based on a separate action).
Best practice to handle this problem is having a local state on your Form Component and managing it locally because I believe it's not a shared state. onSubmit you could dispatch your action passing down the state to the action which is required in making an API call or posting it to your server.
If you try to keep updating your store as the user types, it will keep dispatching the action which might cause problems in future. You read more here Handling multiple form inputs in react

Stack overflow when hot reloading a reducer with React Native and Redux

I'm using Redux with React Native and HMR. When I change a reducer, the accept bubbles up to my configureStore.js file which calls this:
if (module.hot) {
module.hot.accept(() => {
const nextRootReducer = require('../reducers/index').default;
store.replaceReducer(nextRootReducer);
});
}
But then HMR keeps on searching the references in a loop until the stack overflow.
Could it be that there's a circular reference somewhere? If so, any tips for tracking it down? I've tried stepping through the require.js code but the 'references' are kept just as numbers making it hard to track down the right file.
Here's my exact code if you want to examine it:
export default function configureStore() {
engine = createEngine('appstate');
engine = filter(engine, null, ['appStateLoaded', 'appReady', 'tourReady', 'tourPage', 'tooltipStep', 'connected']);
const cacheState = storage.createMiddleware(engine); //OLD PARAMETER: , [LOAD, 'SET_TOUR_READY', 'SET_TOUR_PAGE'] ... replaced by reducer key filtering above instead
enhancer = compose(
applyMiddleware(thunk, cacheState),
devTools({
name: Platform.OS,
hostname: '10.0.1.201',
port: 5678,
filters: {blacklist: ['REDUX_STORAGE_SAVE']}
})
);
//reset reducer state on LOGOUT
const appReducer = combineReducers(reducers);
const rootReducer = (state, action) => {
if (action.type === LOGOUT) {
state = undefined;
}
return appReducer(state, action);
}
reducer = storage.reducer(rootReducer);
store = createStore(reducer, enhancer);
//retreive previously cached state and inject into Redux store!
const load = storage.createLoader(engine);
load(store)
.then((newState) => console.log('Loaded state:', newState))
.catch(() => console.log('Failed to load previous state'));
if(module.hot) {
module.hot.accept(() => {
let nextRootReducer = storage.reducer(rootReducer);
store.replaceReducer(nextRootReducer);
load(store)
.then((newState) => console.log('Loaded state:', newState))
});
}
return store;
}
It should give you a solid example of how to use various middleware with module.hot.accept. ...As far as the "circular reference" thing goes--the only thing to do is check your reducers and make sure they don't import each other. I think I've seen this exact issue in fact--ES6 modules can properly address cycles initial building, but perhaps the HMR build process cannot. And that's all. So you end up thinking you have code that works, but it basically doesn't support HMR. I'd setup a quick test app and implement their hot.module.accept code and verify it works for you. Maybe you'll have to adjust one thing--but since you have so little code, it will be easy to find--and then make that same fix in your own code. If that doesn't surface the issue, then minimize what you're doing with redux in your app temporarily until you isolate what was causing the problem. ..Wish I had more suggestions for ya.

Categories