3.0.0 • Published 9 months ago

@datx/swr v3.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
9 months ago

@datx/swr

React Hooks for DatX


Install

npm install --save swr @datx/swr

Basic usage with Next.js

Datx Client initializer function

For extra SSR setup, see SSR Setup section

// src/datx/createClient.ts

import { Collection } from '@datx/core';
import { CachingStrategy, config } from '@datx/jsonapi';
import { jsonapiSwrClient } from '@datx/swr';

import { Post } from '../models/Post';
import { Todo } from '../models/Todo';

export class JsonapiSwrClient extends jsonapiSwrClient(Collection) {
  public static types = [Todo, Post];
}

export function createClient() {
  config.baseUrl = process.env.NEXT_PUBLIC_JSONAPI_URL as string;
  config.cache = CachingStrategy.NetworkOnly;

  const client = new JsonapiSwrClient();

  return client;
}

export type Client = typeof JsonapiSwrClient;

Client types override

To correctly infer types form expression in useDatx you need to globally override client typings.

// /typings/datx.d.ts

import { Client } from '../src/datx/createClient';

declare module '@datx/swr' {
  export interface IClient extends Client {
    types: Client['types'];
  }
}

Client initialization

// src/pages/_app.tsx

import '@datx/core/disable-mobx';

import type { AppProps } from 'next/app';
import { createFetcher, DatxProvider, useInitialize } from '@datx/swr';
import { createClient } from '../datx/createClient';
import { SWRConfig } from 'swr';

function ExampleApp({ Component, pageProps }: AppProps) {
  const client = useInitialize(createClient);

  return (
    <DatxProvider client={client}>
      <SWRConfig
        value={{
          fetcher: createFetcher(client),
        }}
      >
        <Component {...pageProps} />
      </SWRConfig>
    </DatxProvider>
  );
}

export default ExampleApp;

For more details how to disable Mobx see Disable Mobx section.

Define models

// src/models/Post.ts

import { Attribute, PureModel } from '@datx/core';
import { jsonapiModel } from '@datx/jsonapi';

export class Post extends jsonapiModel(PureModel) {
  public static readonly type = 'posts';

  @Attribute({ isIdentifier: true })
  id!: number;

  @Attribute()
  title!: string;

  @Attribute()
  body!: string;
}
// src/models/Todo.ts

import { Attribute, PureModel } from '@datx/core';
import { jsonapiModel } from '@datx/jsonapi';

export class Todo extends jsonapiModel(PureModel) {
  public static readonly type = 'todos';

  @Attribute({ isIdentifier: true })
  id!: number;

  @Attribute()
  message!: string;
}

It's important to use readonly in public static readonly type = 'posts';. It helps TS to infer the typings in useDatx without the need to manually pass generics.

Define queries

Using expression types (Preferred):

// src/components/features/todos/Todos.queries.ts

import { IGetManyExpression } from '@datx/swr';

import { Todo } from '../../../models/Todo';

export const todosQuery: IGetManyExpression<typeof Todo> = {
  op: 'getMany',
  type: 'todos',
};

Using as const:

// src/components/features/todos/Todos.queries.ts

import { Todo } from '../../../models/Todo';

export const todosQuery = {
  op: 'getMany',
  type: 'todos',
} as const;

It's important to use as const assertion. It tells the compiler to infer the narrowest or most specific type it can for an expression. If you leave it off, the compiler will use its default type inference behavior, which will possibly result in a wider or more general type.

Conditional data fetching

// conditionally fetch
export const getTodoQuery = (id?: string) =>
  id
    ? ({
        id,
        op: 'getOne',
        type: 'todos',
      } as IGetOneExpression<typeof Todo>)
    : null;

const { data, error } = useDatx(getTodoQuery(id));

// ...or return a falsy value, a.k.a currying
export const getTodoQuery = (id?: string) => () =>
  id
    ? ({
        id,
        op: 'getOne',
        type: 'todos',
      } as IGetOneExpression<typeof Todo>)
    : null;

const { data, error } = useDatx(getTodoQuery(id));

// ...or throw an error when property is not defined
export const getTodoByUserQuery = (user?: User) => () =>
  ({
    id: user.todo.id, // if user is not defined this will throw an error
    op: 'getOne',
    type: 'todos',
  } as IGetOneExpression<typeof Todo>);

const { data: user } = useDatx(getUserQuery(id));
const { data: todo } = useDatx(getTodoByUserQuery(user));

Define mutations

// src/components/features/todos/Todos.mutations.ts

import { getModelEndpointUrl, modelToJsonApi } from '@datx/jsonapi';
import { ClientInstance } from '@datx/swr';

import { Todo } from '../../../models/Todo';

export const createTodo = (client: ClientInstance, message: string | undefined) => {
  const model = new Todo({ message });
  const url = getModelEndpointUrl(model);
  const data = modelToJsonApi(model);

  return client.request<Todo, Array<Todo>>(url, 'POST', { data });
};

Use data fetching and mutations together

// src/components/features/todos/Todos.ts

import { useMutation, useDatx } from '@datx/swr';
import { FC, useRef } from 'react';
import { ErrorFallback } from '../../shared/errors/ErrorFallback/ErrorFallback';
import NextLink from 'next/link';

import { createTodo } from './Todos.mutations';
import { todosQuery } from './Todos.queries';

export interface ITodosProps {}

export const Todos: FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const { data, error, mutate } = useDatx(todosQuery);

  const [create, { status }] = useMutation(createTodo, {
    onSuccess: async () => {
      const input = inputRef.current;
      if (input) input.value = '';
      mutate();
    },
  });

  if (error) {
    return <ErrorFallback error={error} />;
  }

  if (!data) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <input ref={inputRef} disabled={status === 'running'} />
      <button onClick={() => create(inputRef.current?.value)} disabled={status === 'running'}>
        add
      </button>

      {data.data?.map((todo) => (
        <NextLink href={`./todos/${todo.id}`} key={todo.id}>
          <a style={{ display: 'block' }}>{todo.message}</a>
        </NextLink>
      ))}
    </div>
  );
};

API

hooks

useInitialize

On the server side it is important to create an entirely new instance of Datx Client for each request. Otherwise, your response to a request might include sensitive cached query results from a previous request.

const client = useInitialize(() => new Client());

useClient

For accessing Client instance from the context. It's made mainly for internal usage.

const client = useClient();

useDatx

const expression: IGetManyExpression<typeof Todo> = {
  op: 'getMany',
  type: 'todos',
};

const config: DatxConfiguration<Todo, Array<Todo>> = {
  // datx config
  networkConfig: {
    headers: {
      'Accept-Language': 'en',
    }
  },
  // SWR config
  onSuccess: (data) => console.log(data.data[0].id),
}

const = useDatx(expression, config);

Second parameter of useDatx is for passing config options. It extends default SWR config prop with additional networkConfig property useful for passing custom headers.

Expression signature

Currently we support 3 expressions for fetching resources getOne, getMany and getAll. Future plan is to support generic request operation and getRelated.

// fetch single resource by id
export interface IGetOneExpression<TModel extends JsonapiModelType = JsonapiModelType> {
  readonly op: 'getOne';
  readonly type: TModel['type'];
  id: string;
  queryParams?: IRequestOptions['queryParams'];
}

// fetch resource collection
export interface IGetManyExpression<TModel extends JsonapiModelType = JsonapiModelType> {
  readonly op: 'getMany';
  readonly type: TModel['type'];
  queryParams?: IRequestOptions['queryParams'];
}

// fetch all the pages of resource collection
export interface IGetAllExpression<TModel extends JsonapiModelType = JsonapiModelType> {
  readonly op: 'getAll';
  readonly type: TModel['type'];
  queryParams?: IRequestOptions['queryParams'];
  maxRequests?: number | undefined;
}

useMutation (deprecated)

A hook for remote mutations This is a helper hook until this is merged to SWR core!

// TODO example

SSR

You will use the fetchQuery method inside getServerSideProps to fetch the data and pass the fallback to the page for hydration.

type HomeProps = InferGetServerSidePropsType<typeof getServerSideProps>;

const Home: NextPage<SSRProps> = ({ fallback }) => {
  return (
    <Hydrate fallback={fallback}>
      <Layout>
        <Todos />
      </Layout>
    </Hydrate>
  );
};

export const getServerSideProps = async () => {
  const client = createClient();

  const todo = await client.fetchQuery(todosQuery);

  return {
    props: {
      fallback: client.fallback,
    },
  };
};

export default Home;

hydrate

type Fallback = Record<string, IRawResponse>

const fallback = {
  '/api/v1/todos': rawResponse
}

<Hydrate fallback={fallback}>

Disable Mobx

Since we don't want to use Mobx, we need to add a little boilerplate to work around that. First we need to instruct DatX not to use Mobx, by adding @datx/core/disable-mobx before App bootstrap:

// src/pages/_app.tsx

import '@datx/core/disable-mobx';

Next, we need to overwrite mobx path so that it can be resolved by datx:

// /tsconfig.json

{
  // ...
  "compilerOptions": {
    //...
    "paths": {
      // ...
      "mobx": ["./mobx.js"]
    }
  }
}

./mobx.js is an empty file!

Troubleshooting

Having issues with the library? Check out the troubleshooting page or open an issue.


Build Status npm version Dependency Status devDependency Status

License

The MIT License

Credits

@datx/swr is maintained and sponsored by Infinum.

3.0.0-beta.1

10 months ago

3.0.0

10 months ago

2.6.2-beta.0

10 months ago

2.6.2-beta.1

10 months ago

2.6.2-beta.2

9 months ago

2.6.1

11 months ago

2.5.1

1 year ago

2.5.0-beta.11

1 year ago

2.5.0-beta.10

1 year ago

2.5.0-beta.8

1 year ago

2.5.0-beta.9

1 year ago

2.5.0-beta.6

1 year ago

2.5.0-beta.7

1 year ago

2.5.0-beta.3

2 years ago

2.5.0-beta.4

2 years ago

2.5.0-beta.5

2 years ago

2.5.0-beta.2

2 years ago

2.5.0-beta.1

2 years ago

2.5.0-beta.0

2 years ago