Skip to content

Instantly share code, notes, and snippets.

@nilscox
Last active June 1, 2022 12:57
Show Gist options
  • Select an option

  • Save nilscox/028b98df7700f02f45285dcc6561c1ed to your computer and use it in GitHub Desktop.

Select an option

Save nilscox/028b98df7700f02f45285dcc6561c1ed to your computer and use it in GitHub Desktop.
SWR dependency inversion
// !! This is not a production-ready snippet !!
// It's just some lines of code I put together to try using SWR with an abstraction layer for the data source
//
// In this example, I use a fake provider (InMemoryTodosProvider) that could be useful for integration testing,
// in storybook, or while the backend is not yet ready. It is then "injected" using a react context.
// The typings isn't quite right, but I don't have much time to do better. Feel free to use this code if you want to.
import { DataProvidersContext, DataProviders } from './data-providers';
import { InMemoryTodoProvider } from './todo.provider';
import { Todos } from './todos';
const providers: DataProvidersContext = {
todosProvider: new InMemoryTodoProvider([
{ id: '1', title: 'Buy some beers', done: false },
{ id: '2', title: 'Finish the dependency inversion example', done: false },
]),
};
export const App: React.FC = () => (
<DataProviders value={providers}>
<Todos />
</DataProviders>
);
import { useCallback } from 'react';
import useSWR, { useSWRConfig } from 'swr';
type Procedure = (...args: any[]) => any;
type ClassMethods<T> = {
[K in keyof T]: T[K] extends Procedure ? T[K] : never;
};
export function useFetch<
Provider,
Methods extends ClassMethods<Provider>,
MethodName extends keyof Methods,
Params extends Parameters<Methods[MethodName]>,
Result extends ReturnType<Methods[MethodName]>,
>(
obj: Methods | undefined,
methodName: MethodName,
...args: Params
): [Awaited<Result>, { loading: boolean; error: unknown; refresh: () => void }] {
const ctor = obj?.constructor.name;
if (obj && typeof obj[methodName] !== 'function') {
throw new Error(`${ctor}.${methodName} is not a function`);
}
const {
data,
error,
isValidating: loading,
mutate: refresh,
} = useSWR(obj ? [ctor, methodName, args] : null, () => obj?.[methodName](...args));
return [data, { loading, error, refresh }];
}
export const useMutate = () => {
const { mutate } = useSWRConfig();
function execute<
Provider,
Methods extends ClassMethods<Provider>,
MethodName extends keyof Methods,
Params extends Parameters<Methods[MethodName]>,
>(obj: Methods, methodName: MethodName, ...args: Params): void {
const ctor = obj?.constructor.name;
if (obj && typeof obj[methodName] !== 'function') {
throw new Error(`${ctor}.${methodName} is not a function`);
}
mutate([ctor, methodName, args], () => obj?.[methodName](...args));
}
return useCallback(execute, [mutate]);
};
import { createContext, useContext } from 'react';
import { TodoProvider, InMemoryTodoProvider } from './todo.provider';
export type DataProvidersContext = {
todosProvider: TodoProvider;
};
const dataProvidersContext = createContext<DataProvidersContext>({
todosProvider: new InMemoryTodoProvider([]),
});
export const DataProviders = dataProvidersContext.Provider;
export const useDataProviders = () => useContext(dataProvidersContext);
type Todo = {
id: string;
title: string;
done: boolean;
};
export interface TodoProvider {
listTodos(): Promise<Todo[]>;
getTodo(id: string): Promise<Todo | undefined>;
createTodo(title: string): Promise<void>;
}
const wait = async (ms: number) => new Promise((r) => setTimeout(r, ms));
export class InMemoryTodoProvider implements TodoProvider {
private todos: Map<string, Todo>;
constructor(todos: Todo[]) {
this.todos = new Map(todos.map((todo) => [todo.id, todo]));
}
async listTodos(): Promise<Todo[]> {
await wait(200);
return Array.from(this.todos.values());
}
async getTodo(id: string): Promise<Todo | undefined> {
return this.todos.get(id);
}
async createTodo(title: string): Promise<void> {
await wait(200);
const id = Math.random().toString(36).slice(-6);
const todo: Todo = { id, title, done: false };
this.todos.set(id, todo);
}
}
export class ApiTodoProvider implements TodoProvider {
async listTodos(): Promise<Todo[]> {
// ...
return [];
}
async getTodo(id: string): Promise<Todo | undefined> {
// ...
return;
}
async createTodo(title: string): Promise<void> {
// ...
}
}
import { FormEventHandler } from 'react';
import { useFetch, useMutate } from './data-fetching';
import { useDataProviders } from './data-providers';
export function Todos() {
const { todosProvider } = useDataProviders();
const [todos, { loading, refresh: refreshList }] = useFetch(todosProvider, 'listTodos');
const mutate = useMutate();
const addTodo: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
mutate(todosProvider, 'createTodo', form.get('title') as string);
refreshList();
};
if (loading) {
return <>Loading todos...</>;
}
return (
<>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<form onSubmit={addTodo}>
<input name="title" placeholder="Add todo" />
</form>
</>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment