Is this a dumb idea for how to simplify redux + react? - reactjs

I'm trying to refactor an app to use redux toolkit but I'm running into an infinite loop when dispatching an action to set state and I suspect its because I'm not following the conventions.
What I'm attempting to do at a high level is have a useAppHandlers hook that returns all the handlers for the entire app. Similarly, I have a useAppState hook that returns a global object with all my state. Snippet from my config file:
export const useAppState = () => {
const coingeckoApiState: AppState[typeof coingeckoApi.reducerPath] = useSelector(state => state.coingeckoApi);
const connectionState: AppState['connection'] = useSelector(state => state.connection);
const currencyState: AppState['currency'] = useSelector(state => state.currency);
return { ...coingeckoApiState, ...connectionState, ...currencyState };
};
export const useAppHandlers = () => {
const connectionHandlers = useConnectionHandlers();
const currencyHandlers = useCurrencyHandlers();
return { ...connectionHandlers, ...currencyHandlers };
};
^^Does that look problematic? I unfortunately can't share the full repo because it's private.
In all the redux examples I've come across, people import useDispatch in addition to the actions they are dispatching within each component. I really don't like how it results in so many lines of code just for imports and set up ex:
const dispatch = useDispatch() repeated ad nauseam across the repo).
This repo is a real-world example of what I'm trying to avoid:
https://github.com/Uniswap/uniswap-interface/blob/4078390a4890445d1ff0ed5196fd4cb56a44de87/src/components/NavigationTabs/index.tsx#L116
Before I give up and just follow conventions, I'd like to pinpoint if the above way I'm configuring redux is the source of the infinite loops, or if its a random bug I introduced deeper in the code.

Honestly, just don't. You will never be able to bundle-split in the future if the need for that arises when creating such a god-object. Also, adding something now means you have to touch at least one more file that is not using that something - same goes for deleting.
In addition to that, writing so "wide" selectors that select a full slice, even if your components only ever consume a part of that slice is a horrible thing for your performance - those components will always rerender if anything in that slice changes, no matter if it is important for your component.
Lastly: Just ignore your imports and let your IDE take care of it for you, probably out of the box. Your IDE can auto-import them for you. You can configure your IDE (with eslint autofix or other plugins) to automatically sort your imports alphabetically and also remove unused imports on save. I'm sure there is even a plugin that will just collapse your imports for you if you don't want to see them.
PS: as for why in react-redux you usually import useDispatch everywhere, you can read a bit on the history of that decision here: https://react-redux.js.org/api/hooks#recipe-useactions

Related

Trying to define a function, which is wrapped in a context, to be used as an onClick handler

I am currently using Laravel Mix + react in a project.
I've tried to simplify my problem as much as possible to eliminate possible unknown variables from laravel-mix.
Thus it boils down to me trying to do the following.
I want to define a function, similiar to the following:
export const exampleFunction = () => {
const appContext = useContext(AppContext);
console.log(appContext.http);
}
This function will need to have the AppContext, but I am not sure as to how this could be achieved, as from what I understand of Laravel mix, you can't encapsulate the whole app and just be able to access each applied context from anywhere.
I would then like to use this function as an onClick on a button, which I think might be even more troublesome to apply on Laravel, but if I find a js only solution I might go with it.

How to use separation of concern with react-query (in a clean architecture context)

I'm currently thinking about the perfect architecture for my professionals projects needs.
I read a lot of article about (clean) architecture and I got to the point were I think that I want my UI managed with React totally separated from the application business logic that will be managed by "application manager". The issue is that I want the "application manager" to config and trigger mutations (I think get queries can be used in components without any issue). But since react-query require it to be in React component by using hooks, I don't think it is possible.
I am wrong ?
Does it exist a workaround ?
Maybe you have a library that manage that better ? I'm thinking about RTK Query maybe...
I am a heavy user of RQ for quite some time and since architecture question can never have an objectively correct answer, I can demonstrate what I do personally.
First, I extract all queries and components into API modules by domain, given a simple app with posts, authors and comments, I would have files along these lines with those exports:
// apis/posts.js
export function useGetPosts() {}
export function useGetPost(postId) {}
export function usePutPost() {}
export function usePostPost() {}
export function useDeletePost() {}
// apis/comments.js
export function useGetComments(postId) {}
export function useGetComment(commentId) {}
export function usePutComment() {}
export function usePostComment() {}
export function useDeleteComment() {}
// apis/authors.js
export function useGetAuthors() {}
export function useGetAuthor(authorId) {}
export function usePutAuthor() {}
export function usePostAuthor() {}
export function useDeleteAuthor() {}
Each of those modules would internally handle everything necessary to work as a whole, like useDeleteAuthor would have a mutation and also modify the cache on success, or possibly implement optimistic updates.
Each will have a system of query keys so that the consumer (your components) don't have to know a thing about them.
function MyComponent() {
const posts = useGetPosts()
}
function MyOtherComponent() {
const deletePost = useDeletePost()
}
Try to make the APIs as complete as possible, but also don't forget that mutations can, for example, accept callbacks on call-site:
deletePost.mutate(payload, {
onMutate: () => setState(false)
})
Let's assume you can use this to for example close a confirmation modal before deleting. Something like this doesn't belong to API module, so we just provide it as a local callback to the mutation.
As stated above, there is no correct answer. There is definitely an argument for doing it the other way round and using collocation more, putting queries next to the components where you are using them. But if you want separation, this would be a place to start in my opinion.
As Ben wrote in the comment to your question, RQ is just hooks, so I agree that trying to put it "outside of react" is non-sensical.
You're right, the short answer is react-query is not compatible with clean architecture, and by experience it leads to tight coupling between logic and components
One way that I'm experimenting with is using the queries in components as is, without implementing side effects. Unless it is side effects specifically for that components.
Then inside my logic layer, I would use the QueryObserver and subscribe to changes to whatever key/keys I need.
const observer = new QueryObserver(myQueryClient, {
queryKey: ['key']
})
observer.subscribe(result => console.log(result))
In this example I have my queryClient defined in its own file.
This way I can have my logic seperated from the view layer, but still use the awesome way react-query works.
Note that this way, the logic will only run when a component is mounted that the query function is resolved.
Also the subscibe function can only be called after the inital useQuery is mounted. Else you will get a "Missing queryFn" error. Which is not ideal. Or even close.

React + Redux share actions between modules/domains

Imagine you have an application with 2 modules (split up in the ducks way).
One is the eagerly loaded Notification module, which is used to display notifications when something succeeds or fails.
The other is a Calculation which makes some calculation
- Notification
- components
- actions
- ...
- index.js
- Calculation
- components
- actions
- ...
- index.js
In a lot of architecture articles they recommend that you should then export the action creators for each module through an index.js file, that sort of functions as the public API of your module.
For example, if I wanted to expose the a success action creator of my Notification module, then I'd export it from the index.js file in that module. Now my other modules can import these action creators.
I like this idea of a public facing API in your module.
What I find troublesome with that way of working is that you then very tightly couple the module to the redux library. Because if I'd switch to a new Notification module, then this module would have to expose action creators too, which is tied to redux.
Is my concern valid? If so, can you suggest a better (but still idiomatic) solution?
What I would do in Angular is the following:
I'd expose from the Notification module a singleton service that acts as the public facing API of this module. If any other module (eg. Calculation) needed to use a feature in the Notification module they could inject the service using dependency injection and call notificationService.addNotification('message'). In that singleton service I would then call the dispatch method on my store.
The Calculation module does not need to know whether the NotificationModule uses a store or not. And I could easily switch around the Notification module, as long as a public facing singleton service still exposed the addNotification method. By inverting the dependencies, I don't need to go change every module that uses the Notification module.
Thanks for your suggestions!
What about using connect function? This way your component
Can be user without Redux at all
dispatch and other similar redux staff will be hidden behind connect
Here is example
export const MyComponent = ({ alertState, notificationsArray, SetAlert, AddNotification }) => {
return <div>
Alert state: {alertState.toString()}
<button onClick={() => SetAlert(!alertState)}>Toggle alert</button>
<div>
Notifications: {notificationsArray.map(n => `${n}, `)}
<button onClick={() => AddNotification()}>Add notification</button>
</div>
</div>
}
export default connect(state => ({ alertState: state.alert.alertState, notificationsArray: state.notifications.notificationsArray }), {...Alerts.actionCreators, ...notification.actionsCreators})(MyComponent)
Note, that inside MyComponent there is no dispatch. So you can use MyComponent without Redux by doing
// Another file
import { MyComponent } from './MyComponent.js'
export const App = () => {
return <MyComponent alertState={true} SetAlert={(alert) => console.log(alert)} notificationsArray={[ 'notification1', 'notification2' ]} AddNotification={() => {}} />
}
Or, if you want to use it as connected, do
// Some third file
import MyComponent from './MyComponent.js' // Note, that now it is default import
export const AnotherComponent = () => {
return <MyComponent />
Now notice, that I don't provide any props to MyComponent a they will be provided by connect.
You can also move call to connect to some other file. So MyComponent will be completely independent of Redux.
You're also not obligated to fully connect MyCompoent to Redux. You can partially connect it
export default connect (state => ({ alertState: state.alert.alertState }), Alerts.actionCreators)(MyComponent)
Now you should provide notifications and AddNotification when invoking MyComponent as they are not taken from Redux.
I think the idiomatic Redux way is for the Calculation module to dispatch an action, and modules interested in that action to handle the action in their reducers. Since all actions are passed to all reducers, this makes for less tight coupling between action dispatcher and action consumer. In this case, the Calculation module doesn't need to care about which components, how many components, or indeed if any components are watching for that action. (Although in most cases, I find that I do create an action producer and one consumer -- and in most cases just one consumer -- of that action, and even though they are loosely coupled, I work on both at the same time.)
I suppose in theory it is possible to create a Notification singleton that you can call from the Calculation module, that would in turn dispatch an action that is handled only by the Notification module itself. I'm not too familiar with how Angular works, but it seems that if you are calling a function exposed by Notification, that creates tight coupling between the components. If you later want to switch out that Notification component for another, would you have to see up all the binding again? And what if other components are interested in the Calculation success? Does the Calculation module have to call functions exposed from singletons in those modules too, introducing more tight coupling?
As with most things, it seems there are pros and cons with both approaches. If you buy into the Redux way of doing things, less tight coupling between components is one of the 'pros', at the expense of less flexiblity should you decide down the road that you want to switch out Redux for a different approach.
You might be thinking/assuming some things incorrectly.
If you think in a sense, Actions/reducers etc are organized and written here in a way that makes those particular modules independent. So, Notification here is independent and so is the Calculation. Code for both are inside their respective folders. Calculation module need not worry of what is happening around the world. Calculations related stuffs are done and relevant actions are dispatched or reducers are updated. Now, if some module (e.g. Notification) wants to do something when Calculation is success, it can listen out for the success dispatch in its own territory.
(Note here that we need not make any changes in the Calculation module for Notification module to work). So, both are decoupled.
What I find troublesome with that way of working is that you then very tightly couple the module to the redux library Yes that is absolutey correct but isn't that what happens when you create a project using some particular framework. You use the syntaxes and the features provided by those libraries but that makes the overall project tightly bound to that library and if you change the library, a lot of code has to be re-written as per the new library or guidelines (unless there is some intelligent compiler). But, this doesn't makes modules coupled (here in redux at least)
This means that when I want to replace my Notification module with another Notification module that doesn't use redux, I'll have to refactor my whole app to not use the dispatch function anymore to create a success. Yes you have to because underlying library has changed. I am not an expert in angular but even in your angular project, if you decide to use something else for the Notification module, I am sure you have to rewrite a lot of stuff in or around the Calculation module for things to work out.
I think what you are saying generally happens if there are very big projects written badly which led to origin of micro-services like architecture. Take some example of e-commerce website. Initially, Authentication, Search, Checkout, Payment (basically backend services) etc were written altogether and so they were tightly coupled. Later on people created micro-services out of them and each of them can communicate with one another using APIs. Now, each of the service and underlying framework can be changed without effecting other but standard APIs are there. Similarly, in frontend as well, you can have such things but it essentially means that you have separate projects altogether which need to communicate and not the modules inside the same project. But it will have same issues be in Redux or in Angular.
Edit: Have updated few points after discussion in comments:
Can you have micro-frontends
Yes, you can have micro-frontends such as Notifications in ReactJsandCalculations in AngularJs and use some public methods such as [window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) or eventListeners but there will be pros and cons to it
Few articles which I found:
Frontend in Microservice Architecture
Micro frontends—a microservice approach to front-end web development
https://micro-frontends.org/
Is it popular right now?
No.
I think some of the reasons being:
Compared to backend, frontend is more of UI/UX and has to look consistent in look and feel wise. So, achieving that might be a bit of issue
Network latency is a big issue and a lot needs to be downloaded. Using 2 or 3 frameworks means you have to download additional data for that framework to work. E.g. both React and Angular library etc. If you see, a lot work goes on reducing the download size which will increase upon increasing the number of frameworks
Most of the websites don't have many pages. Say at max 10-12 different pages and so, creating all of them in one framework is easy and cheap. However, if the project gets big, then big companies do divide. There are very big projects where a.domain.com is in reactJs, b.domain.com is in angular. But that generally happens when projects are big and completely separated from one another.
So, yes you can have it but it depends a lot on factors including but not limited to resources, price, availability etc
If you want to build the micro-frontend, you can use
window.postMessage
EventListeners
Isolating micro-apps into IFrames using libraries and window.postMessage APIs to coordinate. IFrames share APIs exposed by their parent window
Event Emitters (a very good library for the same is - https://github.com/chrisdavies/eev)
Using html5 storage and listening to them for the changes (on in general anything which lets us to play in or around dom/window because that will be the API layer to help us communicate between different modules)
Hope, I am able to clarify if for you. Revert for any doubts/confusion.
To decouple a state-module from the Redux dispatch/action paradigm, you can expose a "public API" via hooks:
For example, suppose your module has an action:
// notifications/actions.js
const createNotification = (data) => ({
type: 'CREATE_NOTIFICATION',
data
});
// ...
In your module, define a hook that returns a function that dispatches the action:
// notifications/hooks.js
import { useDispatch } from 'react-redux';
import { createNotification } from './actions';
function useCreateNotification() {
const dispatch = useDispatch();
return (data) => dispatch(createNotification(data))
}
// ...
Now your component doesn't have to know about dispatch/actions. Just import and use the hooks.
// components/MyComponent.js
import React from 'react';
import { useCreateNotification } from './notifications/hooks'
function MyComponent() {
const createNotification = useCreateNotification();
const handleClick = () => createNotification('foo');
return (
<button onClick={handleClick}>Create Notification</button>
);
}
If you need the public API to expose plain (non-hook) functions, you can do this via a higher-order-function that
takes dispatch and returns a set of functions. For the sake of this example, these functions will be termed "endpoints".
// endpoints.js
import * as actions from './actions';
const createEndpoints = (dispatch) => {
const createNotification = (data) => {
dispatch(actions.createNotification(data))
}
// ...
return {
createNotification,
// ...
}
}
Call the higher-order-function by giving it dispatch:
// store.js
import { createStore } from 'redux';
import rootReducer from './reducer';
import { createEndpoints } from './notifications/endpoints';
export const store = createStore(rootReducer, {});
export const {
createNotification,
// ...
} = createEndpoints(store.dispatch);
Now your UI doesn't have to know about dispatch, actions, or hooks; just call the plain functions as such:
// MyComponent.js
import { createNotification } from './store'
function MyComponent() {
const handleClick = () => createNotification('foo');
return (
<button onClick={handleClick}>Create Notification</button>
);
}
With this approach, you are largely decoupled from a redux implementation. You will still rely on having a redux "dispatch" in order to use the module, but now you are coupled at one point (createEndpoints) instead of many points throughout your components.

is it safe to ignore react's warning about calling the useState hook conditionally when only the parameter is conditional?

I am creating a calendar date selection function component for assigning days to schedules in my React app and I wanted to be able to pre-populate calendar with the existing data so that it could be modified by the user.
This is what I have so far:
const initialOptions: { [key: string]: number[] } = {};
for (const option of Object.keys(props.options)) {
const dates = props.options[option].dates;
initialOptions[option] = dates ? dates : [];
}
const [selectedDates, setSelectedDates] = useState(initialOptions);
However, when I try and render the page, I get this:
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooks
After reading through the react rules of hooks, I didn't see anything that indicated that react was depending on the value of the parameter to "associate local state with [my useState() call]". All it really said was...
As long as the order of the Hook calls is the same between renders, React can associate some local state with each of them.
So why is react complaining at me when I am calling useState() in top-level react code that is outside of any conditional statements or functions as per their own rules?
The comments on this question that basically said calls to react hooks need to be before any control structures, even if they are unrelated were what pointed me in the right direction.
The answer provided in the comments was not quite satisfactory though since I needed to process the inital value of selectedDates if an initial value was provided and have that available before I called useState() in order to pass it as a parameter.
Despite being perplexed by this and the somewhat nonsensical nature of this solution (order shouldn't matter with two barely-related pieces of code, right?), I managed to refactor my logic such that it both stopped react from complaining AND allowed me to still conditionally set the selectedDates in my react calendar component.
Here's what I ended up with:
const initialOptions: { [key: string]: number[] } = {};
Object.entries(props.options).forEach(value => {
const [id, options] = value;
if (options.dates) {
initialOptions[id] = options.dates;
}
});
const [selectedDates, setSelectedDates] = useState(initialOptions);
As someone who isn't that familiar with the internals of react, it seems that either:
the react team got something wrong when writing the ESLint plugin for the react hook rules, or
there was a functional limitation in how ESLint works that doesn't allow for a more specific/accurate check, causing the developers to go with a "better safe than sorry" approach by using a less specific check that still caught rule violations as well as edge cases like this one
So overall, my conclusion is, by replacing my for loop with a call to .forEach(), the ESLint
plugin saw my loop as a function rather than a control structure/conditional and allowed my code to pass the test and run without issue.
Now, as a self-described "junior" react developer, i'm pretty sure that tricking ESLint into not giving an error like this is not a good long-term solution. Ideally ESLint rules probably need updating to better check for improper use of conditionals, and/or the react docs should be updated with an example for how to conditionally set a hook's default value in a way that doesn't violate the rules of react hooks.
EDIT: I have created an issue for the react documentation in order to find out what a good long-term solution to this would be and get the documentation and/or ESLint plugins updated if necessary
If you ignore the warning that means that you are setting your expectations wrong on how your Component's code will be executed during renderings.
Just by looking at initialOptions, you can see that the initial value is based on incoming props. In React when the props change your Component gets re-rendered, the initialOptions is re-evaluated BUT it's NOT updated again by useState(initialOptions).
Sure you can say: "but my useState(initialOptions) is not wrapped around any condition!". While that is absolutely true, you didn't inform React that selectedDates needs to be updated between renders. It's value is still the first initial value when the Component was rendered first time.
You need to move the foreach logic into a useEffect with dependency to props.options.
Example based on your code:
const initialOptions: { [key: string]: number[] } = {};
const [selectedDates, setSelectedDates] = useState(initialOptions);
useEffect(() => {
// Here it's okay to have an optional condition!
if (!props.options.length) { return false; }
const newOptions = [];
Object.entries(props.options).forEach(value => {
const [id, options] = value;
if (options.dates) {
newOptions[id] = options.dates;
}
});
setSelectedDates(newOptions);
}, [props.options]);
I've prepared a sandbox example which demonstrates why the rule "Only Call Hooks at the Top Level - Don’t call Hooks inside loops, conditions, or nested functions." must be respected https://codesandbox.io/s/immutable-meadow-xvy50t?file=/src/Component.js <-- click the body of the page repeatedly and notice that the component doesn't update.

The Complexity of dom manipulation in React + Redux

I'm trying to simplify the workflow when using React and Redux.
For basic DOM Manipulation tasks like scrolling an element down or performing a check for something in an interval, you have to declare many functions and all of a sudden you may have 100 lines of code when there should only be a few lines of code.
The following example is with the react-redux starter kit. Using helper modules such as
createAction, handleActions
The task performed is basicly
var objDiv = document.getElementById("id");
objDiv.scrollTop = objDiv.scrollHeight;
First we have to set our action type in the module
export const SCROLL_DOWN = 'SCROLL_DOWN'
Then we have to define the action for scrolling down
export const setScrollDown = createAction(SCROLL_DOWN, (setScrollDown = true) => setScrollDown)
Then the reducer, where we set the state hasScrolledDown which in turn has to be defined in the module we're in.
export default handleActions({
[SCROLL_DOWN]: (state, { payload }) => {
return {
...state, hasScrolledDown: payload
}
}
}, {hasScrolledDown: false})
Next we have to use all of this in the View
We define the propTypes
class MessagesView extends React.Component {
static propTypes = {
setScrollDown: PropTypes.func,
hasScrolledDown: PropTypes.bool
};
And finally we use them in the componentDidMount & componentDidUpdate functions, to manipulate the dom we need to import the react-dom module, so that goes at the top of the document.
import ReactDOM from 'react-dom'
And then we're finally able to scroll down the page.
componentDidMount () {
this.props.setScrollDown(false)
}
componentDidUpdate (prevProps, prevState) {
if (!this.props.hasScrolledDown) {
ReactDOM.findDOMNode(this).scrollIntoView(false)
this.props.setScrollDown(true)
}
}
So, my question is: Am i going about this the wrong way, or is there a simple solution to handle simple dom-manipulation like this?
As you would imagine when having a large application where you need to perform basic dom-manipulation tasks like this here and there, the codebase grows quite fast.
For DOM stuff like this, like scrolling to a certain point, doing the whole Redux state/action/reducer thing is a bit of an overkill. Of course, this all comes down to personal preference, but there's no need to make things more complicated than they are.
Not knowing your entire application, I'm just making assumptions here, but imagine that your page has a button or something which sole purpose is to scroll the user back to the top of the window. This is something Redux or React shouldn't need to care about, so leveraging it to plain old javascript is a heck of a lot easier to figure about. The way to go about this would usually involve componentDidMount, add a click listener to the button, and take care of the scrolling there.
I even use this approach on tooltips and other GUI stuff that I don't need in a persistent state, because usually it doesn't make sense. Redux is great for keeping track of application state, but that doesn't mean you are forced into using it for every little thing that you don't have to be able to reproduce at any given time.

Resources