From bf5f5688031382578105b9857806b058a0ce897a Mon Sep 17 00:00:00 2001 From: Zheng Song <41896553+szhsin@users.noreply.github.com> Date: Thu, 5 Jan 2023 21:43:09 +1100 Subject: [PATCH 1/2] Add async example --- examples/examples/async/github.ts | 48 ++++++++++++++++++++ examples/examples/async/index.tsx | 54 +++++++++++++++++++++++ examples/examples/async/styles.module.css | 11 +++++ examples/next.config.js | 2 +- examples/pages/async.tsx | 1 + examples/pages/index.tsx | 4 ++ 6 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 examples/examples/async/github.ts create mode 100644 examples/examples/async/index.tsx create mode 100644 examples/examples/async/styles.module.css create mode 100644 examples/pages/async.tsx diff --git a/examples/examples/async/github.ts b/examples/examples/async/github.ts new file mode 100644 index 0000000..b3c60ec --- /dev/null +++ b/examples/examples/async/github.ts @@ -0,0 +1,48 @@ +import { createState, selector } from 'reactish-state'; +import { reduxDevtools } from 'reactish-state/middleware'; + +const state = createState({ middleware: reduxDevtools({ name: 'github' }) }); + +interface UserState { + loading?: boolean; + error?: unknown; + data?: { + name: string; + repos: { id: number; name: string; description: string; stargazers_count: number }[]; + }; +} + +const user = state( + {} as UserState, + (set) => ({ + fetch: async (userName: string) => { + set({ loading: true }, 'user/fetch/pending'); + + try { + const userRes = await (await fetch(`https://api.github.com/users/${userName}`)).json(); + const repoRes = await (await fetch(userRes.repos_url)).json(); + set( + { + data: { + name: userRes.name, + repos: repoRes + } + }, + 'user/fetch/fulfilled' + ); + } catch (error) { + set({ error }, 'user/fetch/rejected'); + } + } + }), + { key: 'user' } +); + +const topRepositories = selector(user, (user) => + user.data?.repos + .slice() + .sort((repo1, repo2) => repo2.stargazers_count - repo1.stargazers_count) + .slice(0, 5) +); + +export { user, topRepositories }; diff --git a/examples/examples/async/index.tsx b/examples/examples/async/index.tsx new file mode 100644 index 0000000..acf5c24 --- /dev/null +++ b/examples/examples/async/index.tsx @@ -0,0 +1,54 @@ +import { useState, useEffect } from 'react'; +import { useSnapshot } from 'reactish-state'; +import { user, topRepositories } from './github'; +import styles from './styles.module.css'; + +const { fetch } = user.actions; + +export default function AsyncExample() { + const [userName, setUserName] = useState('szhsin'); + const { loading, data, error } = useSnapshot(user); + const topRepos = useSnapshot(topRepositories); + + useEffect(() => { + fetch('szhsin'); + }, []); + + const renderUser = () => { + if (loading) return

Loading...

; + if (error) return

Oops! Something went wrong.

; + if (!data) return null; + return ( + <> +

Hi {data.name},

+
Here are your top repositories:
+ + + ); + }; + + return ( +
+ + + {renderUser()} +
+ ); +} diff --git a/examples/examples/async/styles.module.css b/examples/examples/async/styles.module.css new file mode 100644 index 0000000..7bf9b24 --- /dev/null +++ b/examples/examples/async/styles.module.css @@ -0,0 +1,11 @@ +.wrapper { + padding: 1rem; +} + +.desc { + color: #777; +} + +.userInput { + margin: 0 0.5rem; +} diff --git a/examples/next.config.js b/examples/next.config.js index f89c308..953bf58 100644 --- a/examples/next.config.js +++ b/examples/next.config.js @@ -1,6 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: true, + reactStrictMode: false, swcMinify: true }; diff --git a/examples/pages/async.tsx b/examples/pages/async.tsx new file mode 100644 index 0000000..86a5c90 --- /dev/null +++ b/examples/pages/async.tsx @@ -0,0 +1 @@ +export { default } from '../examples/async'; diff --git a/examples/pages/index.tsx b/examples/pages/index.tsx index 71e3648..d0f0f9d 100644 --- a/examples/pages/index.tsx +++ b/examples/pages/index.tsx @@ -12,6 +12,7 @@ export default function Home() {
+

Reactish-State examples

Counter

@@ -19,6 +20,9 @@ export default function Home() {

Todo

+ +

Async

+
From ae0aea8c4b26d85245505a11457cdd0f9c9856b8 Mon Sep 17 00:00:00 2001 From: Zheng Song <41896553+szhsin@users.noreply.github.com> Date: Thu, 5 Jan 2023 22:17:51 +1100 Subject: [PATCH 2/2] Update examples --- examples/examples/counter/Counter.tsx | 36 +++++++------ examples/examples/todo/AddTodo.tsx | 9 +++- examples/examples/todo/Filters.tsx | 3 +- examples/examples/todo/TodoList.tsx | 14 ++--- examples/examples/todo/styles.module.css | 36 ++++++++++++- examples/examples/todo/todo.ts | 5 +- examples/pages/index.tsx | 8 +-- examples/styles/Home.module.css | 67 +----------------------- 8 files changed, 82 insertions(+), 96 deletions(-) diff --git a/examples/examples/counter/Counter.tsx b/examples/examples/counter/Counter.tsx index 75b1e0f..3d36955 100644 --- a/examples/examples/counter/Counter.tsx +++ b/examples/examples/counter/Counter.tsx @@ -16,38 +16,40 @@ const reducer = (state: number, { type, by = 1 }: { type: ActionTypes; by?: numb const persistMiddleware = persist({ prefix: 'counter-', getStorage: () => sessionStorage }); -const counterState = createState({ +const counter = createState({ middleware: applyMiddleware([reduxDevtools({ name: 'counterApp-state' }), persistMiddleware]) })( 0, (set, get) => ({ + // The function updater of `set` receives the current state and should return a new state increase: () => set((i) => i + 1), + // The current state can be also retrieved with the `get` increaseBy: (by: number) => set(get() + by), reset: () => set(0), + // The redux style dispatch function dispatch: (action: { type: ActionTypes; by?: number }) => set((state) => reducer(state, action), action) }), { key: 'count' } ); -const doubleCount = selector(counterState, (count) => count * 2); -const quadrupleCount = selector(doubleCount, (count) => count * 2); -const countSummary = selector( - counterState, - doubleCount, - quadrupleCount, - (count, doubleCount, quadrupleCount) => ({ - doubleCount, - quadrupleCount, - sum: count + doubleCount + quadrupleCount - }) -); +// selector is a piece of derived state from one or more states +const double = selector(counter, (state) => state * 2); + +// selector can be derived from other selectors +const quadruple = selector(double, (state) => state * 2); +const summarySelector = selector(counter, double, quadruple, (count, double, quadruple) => ({ + count, + double, + quadruple, + sum: count + double + quadruple +})); const Counter = ({ id = 1 }: { id: number | string }) => { const [step, setStep] = useState(1); - const count = useSnapshot(counterState); - const summary = useSnapshot(countSummary); - const { increase, increaseBy, reset, dispatch } = counterState.actions; + const count = useSnapshot(counter); + const summary = useSnapshot(summarySelector); + const { increase, increaseBy, reset, dispatch } = counter.actions; console.log(`#${id} count: ${count}`, 'summary:', summary); @@ -67,7 +69,7 @@ const Counter = ({ id = 1 }: { id: number | string }) => {
- + diff --git a/examples/examples/todo/AddTodo.tsx b/examples/examples/todo/AddTodo.tsx index ecc312f..4048995 100644 --- a/examples/examples/todo/AddTodo.tsx +++ b/examples/examples/todo/AddTodo.tsx @@ -1,13 +1,20 @@ import { useState } from 'react'; import { todoListState } from './todo'; +import styles from './styles.module.css'; const AddTodo = () => { const [text, setText] = useState(''); return (
- setText(e.currentTarget.value.trim())} /> + setText(e.currentTarget.value.trim())} + /> + ))} diff --git a/examples/examples/todo/styles.module.css b/examples/examples/todo/styles.module.css index 33ff48c..1d41a5f 100644 --- a/examples/examples/todo/styles.module.css +++ b/examples/examples/todo/styles.module.css @@ -1,10 +1,44 @@ .app { - max-width: 480px; + max-width: 360px; margin: 0 auto; + padding: 1rem; +} + +.todos { + padding: 0; } .todo { display: flex; + align-items: center; justify-content: space-between; + padding: 0.5rem 0.25rem; +} + +.todo:hover { + background-color: rgba(128, 128, 128, 0.2); +} + +.todo:hover .delete { + opacity: 1; +} + +.todoLabel { + flex-grow: 1; +} + +.completed { + text-decoration: line-through; +} + +.add { + margin-left: 0.5rem; +} + +.delete { + opacity: 0; +} + +.filters { margin: 1rem 0; } diff --git a/examples/examples/todo/todo.ts b/examples/examples/todo/todo.ts index 13d1ae1..a5ad155 100644 --- a/examples/examples/todo/todo.ts +++ b/examples/examples/todo/todo.ts @@ -17,13 +17,16 @@ const todoListState = state( [] as Todo[], (set, get) => ({ addItem: (text: string) => + // The function updater of `set` receives the current state and should return a new state immutably set((todos) => [...todos, { id: Date.now(), text, isCompleted: false }], 'todos/addItem'), toggleItem: (id: number) => + // The current state can be also retrieved with the `get` set( get().map((item) => (item.id === id ? { ...item, isCompleted: !item.isCompleted } : item)), { type: 'todos/toggleItem', id } ), deleteItem: (id: number) => + // You can also mutate the state because we have set up the `immer` middleware set( (todos) => { const index = todos.findIndex((item) => item.id === id); @@ -37,7 +40,7 @@ const todoListState = state( ); type VisibilityFilter = 'ALL' | 'COMPLETED' | 'IN_PROGRESS'; -const visibilityFilterState = state('IN_PROGRESS' as VisibilityFilter, null, { key: 'filter' }); +const visibilityFilterState = state('ALL' as VisibilityFilter, null, { key: 'filter' }); const selector = createSelector({ plugin: devtoolsPlugin({ name: 'todoApp-selector' }) }); const visibleTodoList = selector( diff --git a/examples/pages/index.tsx b/examples/pages/index.tsx index d0f0f9d..36f6318 100644 --- a/examples/pages/index.tsx +++ b/examples/pages/index.tsx @@ -2,17 +2,19 @@ import Head from 'next/head'; import Link from 'next/link'; import styles from '../styles/Home.module.css'; +const TITLE = 'Reactish-State examples'; + export default function Home() { return (
- Examples - + {TITLE} +
-

Reactish-State examples

+

{TITLE}

Counter

diff --git a/examples/styles/Home.module.css b/examples/styles/Home.module.css index bd50f42..5676567 100644 --- a/examples/styles/Home.module.css +++ b/examples/styles/Home.module.css @@ -12,59 +12,6 @@ align-items: center; } -.footer { - display: flex; - flex: 1; - padding: 2rem 0; - border-top: 1px solid #eaeaea; - justify-content: center; - align-items: center; -} - -.footer a { - display: flex; - justify-content: center; - align-items: center; - flex-grow: 1; -} - -.title a { - color: #0070f3; - text-decoration: none; -} - -.title a:hover, -.title a:focus, -.title a:active { - text-decoration: underline; -} - -.title { - margin: 0; - line-height: 1.15; - font-size: 4rem; -} - -.title, -.description { - text-align: center; -} - -.description { - margin: 4rem 0; - line-height: 1.5; - font-size: 1.5rem; -} - -.code { - background: #fafafa; - border-radius: 5px; - padding: 0.75rem; - font-size: 1.1rem; - font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, - Bitstream Vera Sans Mono, Courier New, monospace; -} - .grid { display: flex; align-items: center; @@ -103,11 +50,6 @@ line-height: 1.5; } -.logo { - height: 1em; - margin-left: 0.5rem; -} - @media (max-width: 600px) { .grid { width: 100%; @@ -116,14 +58,7 @@ } @media (prefers-color-scheme: dark) { - .card, - .footer { + .card { border-color: #222; } - .code { - background: #111; - } - .logo img { - filter: invert(1); - } }