Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion: Provide jotai/nextjs utils #340

Closed
Thisen opened this issue Mar 5, 2021 · 25 comments
Closed

Discussion: Provide jotai/nextjs utils #340

Thisen opened this issue Mar 5, 2021 · 25 comments
Assignees

Comments

@Thisen
Copy link
Collaborator

Thisen commented Mar 5, 2021

We already provide some examples about how to use Jotai with Next.js and SSR in general.

This issue looks to start a discussion if it's possible for Jotai for provide a better story for using Jotai in a SSR scenario - We believe we could start with Next.js.

I believe this would be mostly useful in the scenario, where you have async atoms, which fetch their state using e.g. fetch.

This is mostly inspired Next.js story for react-query: https://react-query.tanstack.com/guides/ssr

Let me think what you think - Should we provide something, or should we just document how to with our current API for "hydration" of atoms.

@Aslemammad
Copy link
Member

Aslemammad commented Mar 5, 2021

We have something like initialData in react-query, which works in Provider; I think we update it to work with provider-less mode. I think we can put it in 1-useAtom(atom, InitialData) or 2-atom.initialData, I think I prefer the first option.

1- const [data, setData] = useAtom(atom, initialData)
2- atom.initialData = {}

@aulneau
Copy link
Collaborator

aulneau commented Mar 5, 2021

I would agree, I think there is a lot of room for improvement and I bet it could be such that ssr can work kind of just out of the box, eg I define an atom that has an ability to fetch its own state, and it just works with loading the initial data out of the box.

@Thisen
Copy link
Collaborator Author

Thisen commented Mar 5, 2021

I was thinking along the same lines @aulneau, but I didn't want to come with any solutions, as this should be a discussion.

@Thisen
Copy link
Collaborator Author

Thisen commented Mar 5, 2021

Btw, I'm happy to work on any ideas that comes out of this.

@Aslemammad
Copy link
Member

Let's do more discussions on that with @dai-shi and other folks; then, we'll work on it. Thanks for the issue!

@aulneau
Copy link
Collaborator

aulneau commented Mar 5, 2021

For sure :~) I don't mean to imply any correct solutions, and would be happy to help out too!

@Thisen
Copy link
Collaborator Author

Thisen commented Mar 5, 2021

I believe a great place to start would be getServerSideProps and getStaticProps and give them a simplistic API to hydrate atoms.

Are there other use cases I'm not thinking of?

@dai-shi
Copy link
Member

dai-shi commented Mar 5, 2021

Thanks for opening this issue up @Thisen .

Let me think what you think - Should we provide something, or should we just document how to with our current API for "hydration" of atoms.

I'm pretty sure there would be something to provide, but instead of thinking what is the thing, we would like to work on the best practices (especially because I'm not super familiar with next, yet). So the steps are:

  1. Discuss in this issue
  2. Improve docs and examples
  3. And then, add a new util if it makes sense.

@dai-shi
Copy link
Member

dai-shi commented Mar 5, 2021

I think we update it to work with provider-less mode.

@Aslemammad well, think it like the provider is left for use cases such as nextjs. we don't need to support provider-less mode and it's probably impossible. See the related discussion in zustand for example. Besides, changing the core api is the least thing we'd like to do. Keep it minimalistic. Let's do things on top of the core.

@Thisen Thisen self-assigned this Mar 6, 2021
@sandren
Copy link
Collaborator

sandren commented Mar 7, 2021

Keeping the <Provider> component in Jotai's core API kind of already is supporting SSR in core. In light of that I think any API improvements related to Jotai usage in an SSR context should continue to be a part of core rather than new framework-specific utils, especially since Next.js is only one of several React-based frameworks that support SSR. 😀

@dai-shi
Copy link
Member

dai-shi commented Mar 7, 2021

jotai/nextjs would be very specific to nextjs depending on functions from nextjs. If there were any generic SSR utils, we should put them in jotai/utils.

@Nazzanuk
Copy link

Nazzanuk commented Aug 6, 2021

So I'm working on a semi large site and we are using the current philosophy which seems to work for us. Would appreciate thoughts on this:

_app.tsx has a top level provider

// _app.tsx
import { JotaiNextProvider } from "app/JotaiNextProvider";

const App = ({ Component, pageProps }) => (
  <JotaiProvider pageProps={pageProps}>
    <Component {...pageProps} />
  </JotaiProvider>
);

export default App;

This provider is custom, it wraps the Jotai Provider and sets initial state to an atom (appPropsAtom) which can then be used to derive new atoms.

// JotaiNextProvider.tsx
import { Provider } from "jotai";
import { useEffect } from "react";
import { useUpdateAtom } from "jotai/utils";
import { appPropsAtom } from "app/appAtoms";

const JotaiContainer = ({ pageProps, children }) => {
  const setInitialProps = useUpdateAtom(appPropsAtom);

  useEffect(() => {
    setInitialProps(pageProps);
  }, [pageProps]);

  return <>{children}</>;
};

const JotaiNextProvider = ({ pageProps, children }) => (
  <Provider initialValues={[[appPropsAtom, pageProps]]}>
    <JotaiContainer pageProps={pageProps}>{children}</JotaiContainer>
  </Provider>
);

export { JotaiNextProvider };

Top level atom and derived atom

// appAtoms.tsx
import { atom } from "jotai";

// We can just derive atoms from this
const appPropsAtom = atom({});

// Derived atoms
const apiContentAtom = atom((get) => get(appPropsAtom)?.apiContent);
const otherApiContentAtom = atom((get) => get(appPropsAtom)?.otherApiContent);

export { appPropsAtom, apiContentAtom, otherApiContentAtom };

Wherever we need SSR content we pass it into the top level atom and the use it in the page or any components without having to prop drill

// pages/index.tsx
import { useAtomValue } from "jotai/utils";
import { apiContentAtom } from "appAtoms";

// Works for the other SSR functions as well
export async function getStaticProps() {
  const apiContent = await fetchFromApi();

  return { props: { apiContent } };
}

export default function IndexPage() {
  // Read the atom and it will have the correct data when SSR
  const apiContent = useAtomValue(apiContentAtom);

  return <div>SSR content: {apiContent}</div>;
}

sandbox example

@Thisen
Copy link
Collaborator Author

Thisen commented Aug 6, 2021

Great input @Nazzanuk. Could you maybe look into #637 - Can you maybe try it out?

@dai-shi
Copy link
Member

dai-shi commented Aug 6, 2021

@Nazzanuk Thanks for share your work.
If I understand it correctly, useEffect in JotaiContainer is required because pageProps changes on client side rendering?
(@Thisen In such a case, #637 doesn't help? what do you think?)

@Thisen
Copy link
Collaborator Author

Thisen commented Aug 6, 2021

@dai-shi AFAIK, props will be there on initial render.

@Nazzanuk
Copy link

Nazzanuk commented Aug 6, 2021

@Nazzanuk Thanks for share your work.
If I understand it correctly, useEffect in JotaiContainer is required because pageProps changes on client side rendering?

Yes it's to update the props, which are present on initial render and are also updated when navigating client-side.

@Nazzanuk
Copy link

Nazzanuk commented Aug 6, 2021

So looking at #637, am I right in saying that useHydrateAtoms would prevent the need for a Provider at all? And save quite a bit of boilerplate.

The above implementation could be rewritten as:

// _app.tsx
const App = ({ Component, pageProps }) => {
  useHydrateAtoms([[appPropsAtom, pageProps]]);
  const setAppProps = useUpdateAtom(appPropsAtom);

  useEffect(() => {
    setAppProps(pageProps);
  }, [pageProps]);

  return <Component {...pageProps} />;
};

@dai-shi
Copy link
Member

dai-shi commented Aug 6, 2021

Yes, that's the idea.

@Thisen
Copy link
Collaborator Author

Thisen commented Aug 10, 2021

Hi everyone, there's a new hook useHydrateAtoms - Please try it out in your Next.js app!

@Thisen
Copy link
Collaborator Author

Thisen commented Aug 10, 2021

As useHydrateAtoms is a solution for hydrating atoms and we have #485 for syncing an atom with a router, I'm closing this issue. If you have any use cases, feel free to open a new issue.

@Thisen Thisen closed this as completed Aug 10, 2021
@sondh0127
Copy link

hi guys, can u give me an example of how to use useHydrateAtoms together with atomWithQuery in nextjs project

@Thisen
Copy link
Collaborator Author

Thisen commented Feb 21, 2022

Hi @sondh0127 👋🏼 - I'm assuming it's the react-query atomWithQuery you are referring to. I haven't tried this out, but my guess to use your own QueryClient instance and then follow this guide for hydrating the QueryClient.

@sondh0127
Copy link

image
Thank for your suggestion @Thisen. I follow the guide to setup the client, but my question is how to use my own client with atomWithQuery

@Thisen
Copy link
Collaborator Author

Thisen commented Feb 22, 2022

That's the second argument of atomWithQuery:

const idAtom = atom(1)
const userAtom = atomWithQuery((get) => ({
  queryKey: ['users', get(idAtom)],
  queryFn: async ({ queryKey: [, id] }) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    return res.json()
  },
}), queryClient)

@sondh0127
Copy link

sondh0127 commented Feb 22, 2022

Thank @Thisen. I've gotten the final code to work. Sharing it here in case someone is looking for it.
Also It's

"next": "^12.1.0",
"react": "^18.0.0-rc.0",
"react-dom": "^18.0.0-rc.0",

_app.tsx

export const queryClientAtom = atom(new QueryClient())

function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  const [queryClient] = useAtom(queryClientAtom)

  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
         ...
      </Hydrate>
    </QueryClientProvider>
  )
}

state.tsx

const idAtom = atom(1)
const userAtom = atomWithQuery(
  (get) => ({
    queryKey: ['users', get(idAtom)],
    queryFn: async ({ queryKey: [, id] }) => {
      const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
      return res.json()
    },
  }),
  (get) => get(queryClientAtom)
)

const User = () => {
  const [user] = useAtom(userAtom)

  return (
    <div>
      <i>
        {user.name} | @{user.username}
      </i>
      <br />
      <i>{user.phone}</i>
      <br />
    </div>
  )
}

const StatePage: NextPageWithLayout = ({}) => {
  return (
    <>
      <Provider>
        <Suspense fallback="Loading...">
          <User />
        </Suspense>
      </Provider>
    </>
  )
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants