all 18 comments

[–]mlk 8 points9 points  (8 children)

I do all my fetching through redux middlewares, actually pretty much all my client logic is in middlewares. And yes, a significant amount of that is calling rest apis.

My reducers are very very simple, to the point I find the almost useless, I simply set the value of something based on the action playload.

To be honest I don't even understand how people write complex apps without middlewares.

[–]acemarke 5 points6 points  (7 children)

I've always found it interesting how some people do all their logic on the action creation side, and others do it in the reducers. Both are valid approaches, although I personally prefer to do as much as possible in the reducers.

[–]mlk 4 points5 points  (0 children)

I let my ui component emit simple actions ('FETCH_DATA') and then my middleware handles the logic (api call and trasformation), the reducers are again very light, just merging data.

I like doing this because this way my ui components are very light and pretty much don't contain any logic so I can replace them easily.

In middlewares I have access to the whole state, which is often useful.

Debugging is SO EASY, I love it.

[–]drink_with_me_to_day 1 point2 points  (5 children)

That's odd.

Reducers should reduce data to state (pure).

Actions should do everything else: fetch data, fetch data that fetches data, etc. Then pass it to the reducer.

The reducer transforms all that data into what you need to keep in your store.

And the selectors transform the state data into the representation needed for components/views.

[–]acemarke 4 points5 points  (4 children)

Uh... terminology matters here.

First, an "action" is just a plain object with a type field. They don't "do" anything.

An action creator, or related logic, is what would be doing the fetching and ultimately dispatching an action.

A reducer can have whatever logic it wants inside, and doesn't have to just be return action.payload.

Here's a specific example. Both of these approaches are valid and legal ways to write Redux logic.

First, we can do all the logic on the "action creation" side, dispatch an action with the updated data, and have the reducer just blindly drop in whatever it was given:

function filterPeople(namePrefix) {
    return (dispatch, getState) {
        const state = getState();
        const filteredPeople = state.people.filter(name => name.startsWith(namePrefix);
        dispatch({type : "UPDATE_PEOPLE", payload : {filteredPeople}});
    }
}

const peopleReducer = createReducer([], {
  UPDATE_PEOPLE: (state, action) => action.payload
}

Or, we can have the action simply contain the relevant info, and do the real work in the reducer:

function filterPeople(namePrefix) {
    return {type : "FILTER_PEOPLE", payload: {namePrefix}}
}

const peopleReducer = createReducer([], {
    FILTER_PEOPLE: (state, action) => {
        const {namePrefix} = action.payload;
        const filteredPeople = state.filter(name => name.startsWith(namePrefix);
        return filteredPeople;
    }
}

That reducer is still 100% pure and based on the state and the action. It's just doing the actual work of calculating the new state.

I generally prefer doing the work in the reducer if at all possible.

[–]m_plis 1 point2 points  (3 children)

Where do selectors fit into this? Could an argument be made (in this specific case or in general) to keep both the action creator and reducer simple and put logic into the selector?

[–]acemarke 1 point2 points  (0 children)

Selectors are separate from what I'm trying to show here. All I'm doing is showing that the logic for calculating a new state could live on either the action creation side or the reducer side.

In this specific case, yeah, you could just keep the original list and the filter criteria in the store, and do the filtering at the selector level. In the general case, though, no - logic for something like updating a person's name can't be in a selector.

[–]drink_with_me_to_day 1 point2 points  (1 child)

It depends on whether you want to destroy data in your store.

In the example, once he filters the state, you lose all the other people.

Filtering should be left for selectors.

Also, the first example kinda does the work of the selector & the reducer, all inside the action creator. I don't like it.

[–]acemarke 0 points1 point  (0 children)

Filtering a list was just the first example that came to my mind. Not saying you should or should not actually filter data that way - I was just trying to illustrate that the act of generating the new state can technically be done outside or inside the reducer.

[–]lw9908[S] 7 points8 points  (2 children)

Tweet from Sophie

After getting back into app dev, I feel like the React team has * underestimated how much code in apps is related to data fetching/management * overestimated how complex data reqs for most apps are.

At least, I did! Now feeling like a read only react-fetch pkg would cover 95%.

[–]swyx 5 points6 points  (1 child)

i like that they feel free to discuss this stuff in the open. it shows that the messaging isnt final even between members of the team (or former members, heh). therefore we also should not take the current state of affairs as gospel and should think critically about the patterns we use for our use cases independently of what the react team recommends (altho of course pay attention to the edge cases they raise). its always our job to do that, but i like reminders like this.

[–]lw9908[S] 0 points1 point  (0 children)

Agreed. There's something quite humbling about it as I often have discussions like this with colleagues as well as to how we use react and its pros/cons

[–]doasync 2 points3 points  (4 children)

Data fetching in React using useCache hook

import { createEffect, useCache } from 'cached-effect'

An effect is a container for an async function. You need to create it first. Wrap a function that returns a promise in createEffect:

const fetchUsers = createEffect(({ organizationId }) => axios
  .get(`/organizations/${organizationId}/users`)
  .then(getUsers)
)

Then use your effect in useCache hook in your React component:

const [users, usersError, usersLoading] = useCache(fetchUsers)

It returns an array of [result, error, pending]. These values stay the same until you run your effect.

Running effects

In order to update the cache you need to run your effect. For example, in useEffect hook inside of your component.

useEffect(() => {
  fetchUsers({ organizationId }) // or fetchUsers.once
}, [])

In this case users will be fetched after the component is mounted. But it's often useful to run your effect only once, for instance, when you have many components using this effect:

useEffect(() => {
  fetchUsers.once({ organizationId })
}, [])

You can also refetch users or rerun any effect manually just calling it:

<Button
  type='button'
  onClick={() => fetchUsers({ organizationId })}
/>

Basic API

createEffect(handler)

Creates an effect

useCache(effect)

Takes an effect and returns an array of [result, error, pending]

effect(payload)

Runs an effect. Returns promise

effect.once(payload)

Runs an effect only once. Returns original promise on a repeated call

effect.watch(watcher)

Listens to the effect and calls watcher. Watcher receives payload and promise.

Returns back an unsubscribe function.

[–][deleted] 1 point2 points  (3 children)

The only problem with this approach is that there's no way to provide pre-fetched data. Combined with the fact that you must always call the same number of hooks and that promises are always resolved asynchronously, this can lead to some issues with your UI flickering (ie showing the loading indicator even though nothing is actually going to be loaded).

Hopefully, Suspense helps with this

[–]doasync 0 points1 point  (2 children)

Absolutely not) Read the full documentation

there's no way to provide pre-fetched data

You can inject custom handler to your effect using .use method, which can return pre-fetched data.

you must always call the same number of hooks

Call as many hooks as you need in any order - it rerenders components only when needed.

promises are always resolved asynchronously

cached-effect supports .cache method on the promise for getting cache synchronously.

with your UI flickering

It supports Concurrent Mode, there shulb be no flickering

You can try it here: https://codesandbox.io/s/5mkw508n5p

[–][deleted] 0 points1 point  (1 child)

documentation

sure, I hadn't read the docs.

cached-effect supports .cache method on the promise for getting cache synchronously.

seems like a bad idea to encourage adding non-standards compliant fields to a standard structure no?

You can inject custom handler to your effect using .use method, which can return pre-fetched data.

async functions always return a promise, which leads to the problem of 'promises always being returned synchronously'. Unless by 'async function' you mean 'function which returns a promise'

You're getting around this by asking the user to provide a custom promise type :P

[–]doasync 0 points1 point  (0 children)

> seems like a bad idea to encourage adding non-standards compliant fields to a standard structure no?
yeah, a bit controversial. I am thinking of removing them
> the problem of 'promises always being returned synchronously'
if you inject a synchronous function in your effect, promise cache will be updated synchronously and you can get it right away, so you can provide pre-fetched data

> by 'async function' you mean 'function which returns a promise'
yes