Last active
June 1, 2022 12:57
-
-
Save nilscox/028b98df7700f02f45285dcc6561c1ed to your computer and use it in GitHub Desktop.
SWR dependency inversion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // !! 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> | |
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> { | |
| // ... | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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