4

I've recently started using react using functional components and react-query and it has been working fine except that I'm not clear about how to organize components properly.

the way I'm used to designing components is by having a top level component that performs all data access and passes data to it's child components through props. it also passes various callback handlers to child components so that when action is required, the top level component would update the data and passes the new data to child component. so in my case all calls to useQuery(), useMutation() reside in top level component but it's making the code extremely messy. but it's much like a page containing various child components that only display data or help user interact with data.

function Page(){
  const [page, setPage] = useState(1)
  const [size, setSize] = useState(10)

  const persons = useQuery('persons', async ()=> await getPersons(page, size))
  const addPerson = useMutation(async (args)=> { 
   const {id, name, desc} = args
await addPerson(id, name, description)
})
  const person = useQuery('persons', async ()=> await getOnePerson(page, size), { enabled : false })

  const addPersonCB = (id: number, name: string, desc: string)=> {
    addPerson.mutate({id, name, desc})
  }
 // complex if/else logic to choose child components 

the second approach is to disperse react useQuery() and useMutation throughout the components where it's need. and to further simplify things, if rendering logic is complex, each component would have a parent component that would perform the action and passes data as prop.

function PersonCard(props: PersonCardPropsType){
   const {data, isLoading, isError, error} = useQuery(`personQuery${props.id}`, getPerson)

   if(isLoading)
      return <Wait />
  
   if(isError)
     return <Error reason={error} />

   const record = data as PersonModel
   return ( <PersonCardUI person={record} />) 
}

and there are may compoenents for grid, form and etc each one in form of pair like

<PersonEditor />, <PersonEditorUI />, <PersonGrid />, <PersonGridUI /> 

in this case the calls are dispersed everywhere in the code. I want to know

  1. For large projects, which approach is recommended and why?
  2. Is the mix-match of Redux & react-Query okay? like for instance a grid has page size and page number which should go in redux, maybe?
  3. Is it okay to use pure axios/fetch at some places with redux/react-query it's considered a frowned upon way of doing things?
Simple Fellow
  • 4,315
  • 2
  • 31
  • 44
  • All of these boil down to opinion. For #1 it depends on your needs; we use the r-q hooks where they're needed and don't pass things down through layers of components (but we also use contexts for some of it if it's shared data). #2 of course, although I rarely use Redux for UI-only concerns--that's what state is for. #3 use whatever makes the most sense in context. – Dave Newton Jan 04 '22 at 17:04
  • 1
    I wrestled w/this same issue not too long ago where I was using [useSwr](https://swr.vercel.app/). I took a hybrid approach. I had custom hooks around my `useSwr`'s & I lifted state up as much as I could. I then used redux for global state such as auth user. Plus I used react's context api when lifting state up turned out to be too cumbersome and redux seemed like overkill. I also had filters whose state was in the url itself. All in all this mixed approach seemed to have worked very well. – Sangeet Agarwal Jan 04 '22 at 17:09
  • so what you are saying, "no approach is wrong and it all depends on one's needs" but if that's the approach we follow in large project, won't it become a code mess after some time where 3-4 devs are working with their own opinions ? – Simple Fellow Jan 04 '22 at 17:10
  • right... I think you could always argue merits/demerits of a given approach but I doubt there's one way of doing things. It will be a bit of a judgement call. I do think one can't do away with redux though & love using it for global state, as long as it is used sparingly. – Sangeet Agarwal Jan 04 '22 at 17:14

2 Answers2

11

It is generally considered a best practice to use useQuery where you need it. The separation into container / presentational components, while still possible, has been largely deprecated since hooks came around. With redux connect / mapStateToProps, it was best practice. Now, even in redux, you just call useSelector and useDispatch close to where you need it. This is no different in react-query.

There is a great talk on this subject from Mark Erikson: Hooks, HOCs and tradeoffs that I can totally recommend watching.

Using react-query hooks where they are needed not only avoids prop drilling, it also makes it easier for react-query to keep your data up-to-date, because more observers (=components that call useQuery) are mounting. This is also why it's best to just set a staleTime when you want to customize refetching behaviour. I've written about this in detail in React Query as a State Manager.

Is the mix-match of Redux & react-Query okay? like for instance a grid has page size and page number which should go in redux, maybe?

Totally, as long as you don't sync server state to redux. page number and page size are considered "client state" because the client is control over that state. The user selects the page, and the server responds with the data depending on it. I also like to abstract that away together in custom hooks:

const useData = () => {
  const pageNumber = useSelector(state => state.pageNumber)
  return useQuery(["data", pageNumber], () => fetchData(pageNumber))
}

that way, you have a hook you can use wherever you want (without passing anything to it), and it re-fetches data automatically if the pageNumber changes.

Is it okay to use pure axios/fetch at some places with redux/react-query it's considered a frowned upon way of doing things?

If you don't need caching / loading states managed for you etc then sure. The only thing that comes to my mind where I don't want a query / mutation might be file downloads or so :)

TkDodo
  • 20,449
  • 3
  • 50
  • 65
  • I totally understand why using `useQuery` in the component you need it's considered a best practice in a _hooks world_. However, I don't see how this can work in a SSR context like Next.js, where you would like to prefetch the query in `getStaticProps` or `getServerSideProps`. Are you supposed to recursively inspect all the components used in the page, find all the calls to `useQuery` and then replicate them with the `prefetchQuery` method? – toioski May 11 '23 at 15:13
1

Let’s step back for a minute and see what each of these abstractions help us achieve & then it makes it easier to see how one should architect an application. Very broadly, you have

  1. useQuery useSwr

  2. Redux or any other global state management tool

  3. concept of lift state up

  4. Context api

  5. Saving state in url (filters say or link to say a product/item page)

useQuery useSwr are responsible for managing remote state and provide a snapshot of your data that resides behind a remote API. They help with fetching data, caching, error handling, showing loading spinners. They give us additional features such as refetch after a certain interval or refetch on focus, refetch a certain # of times on error etc. Whether we then decide to call these individually in each component or a parent component is a matter of design i.e. implementation detail.

Redux and other global state management tool help with managing local state, globally throughout your client application. A great example of that would be your auth’ed user. That information probably is required globally so redux sounds like a great place to have that information. Shopping cart is another example that might make sense in a redux store.

Lift state up when you want to share information with siblings. This stackoverflow question is a perfect example of lifting state up. DataTableComponent now becomes a controlled component or what you might call a presentation component.

If lifting state up becomes too cumbersome then look at context api or perhaps redux.

So, taking shopping cart as an example, you might decide that context api makes better sense or perhaps lifting state up makes more sense rather than having it in a redux store. My point being that there isn't one way of doing this and it will be a judgement call.

Lastly, you might have a page with filters say, and you may want to give your users an ability to send a link/Url & you might want the recipients to see the same information as the sender. So, now you must save state in your url via say query strings.

Going back to my comment above, there is no one way of doing things. So, you may start off by lifting state but then realize it's too cumbersome so you may switch to context api or even redux.

But each of these abstractions usually do have a place in your application & I have used all the above abstractions in conjunction with each other quite successfully.

Sangeet Agarwal
  • 1,674
  • 16
  • 25