├── .nvmrc ├── website ├── .gitignore ├── docs │ ├── releases │ │ └── README.md │ ├── assets │ │ ├── devtools.png │ │ ├── happy-peas.png │ │ ├── logo-small.png │ │ ├── screencast.mp4 │ │ ├── devtools-thunk.png │ │ ├── devtools-action.png │ │ ├── devtools-listenaction.png │ │ ├── devtools-listenthunk.png │ │ └── typescript-tutorial │ │ │ ├── typed-hooks.png │ │ │ ├── typing-model.png │ │ │ ├── typed-action-imp.png │ │ │ ├── typed-get-state.png │ │ │ ├── typed-thunk-imp.png │ │ │ ├── typed-computed-imp.png │ │ │ ├── typed-thunk-globals.png │ │ │ ├── typed-action-dispatch.png │ │ │ ├── typed-injections-imp.png │ │ │ └── typed-thunk-dispatch.png │ ├── docs │ │ ├── tutorials │ │ │ ├── extended-api.md │ │ │ └── README.md │ │ ├── known-issues │ │ │ ├── README.md │ │ │ ├── using-keyof-in-generic-typescript-model.md │ │ │ └── typescript-optional-computed-properties.md │ │ ├── api │ │ │ ├── action-on.md │ │ │ ├── generic.md │ │ │ ├── thunk-on.md │ │ │ ├── README.md │ │ │ ├── store-provider.md │ │ │ ├── use-store.md │ │ │ ├── use-store-dispatch.md │ │ │ ├── use-store-actions.md │ │ │ ├── debug.md │ │ │ ├── use-store-rehydrated.md │ │ │ ├── create-store.md │ │ │ └── create-transform.md │ │ ├── recipes │ │ │ ├── README.md │ │ │ ├── react-native-devtools.md │ │ │ ├── connecting-to-reactotron.md │ │ │ ├── hot-reloading.md │ │ │ └── usage-with-react-redux.md │ │ ├── typescript-api │ │ │ ├── README.md │ │ │ ├── state.md │ │ │ ├── actions.md │ │ │ ├── reducer.md │ │ │ ├── create-typed-hooks.md │ │ │ ├── action.md │ │ │ ├── action-on.md │ │ │ ├── effect-on.md │ │ │ ├── computed.md │ │ │ └── thunk-on.md │ │ ├── introduction │ │ │ ├── installation.md │ │ │ ├── architecture.md │ │ │ └── README.md │ │ └── community-extensions │ │ │ └── README.md │ └── .vuepress │ │ └── public │ │ ├── favicon.png │ │ ├── happy-peas.png │ │ └── camera.svg └── package.json ├── .eslintignore ├── .npmrc ├── examples ├── react-native-todo │ ├── .node-version │ ├── .watchmanconfig │ ├── .ruby-version │ ├── .bundle │ │ └── config │ ├── tsconfig.json │ ├── app.json │ ├── .eslintrc.js │ ├── babel.config.js │ ├── resources │ │ └── todo.gif │ ├── android │ │ ├── app │ │ │ ├── debug.keystore │ │ │ ├── src │ │ │ │ ├── main │ │ │ │ │ ├── res │ │ │ │ │ │ ├── values │ │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ │ └── drawable │ │ │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ └── java │ │ │ │ │ │ └── com │ │ │ │ │ │ └── reactnativetodo │ │ │ │ │ │ └── MainActivity.java │ │ │ │ ├── debug │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── release │ │ │ │ │ └── java │ │ │ │ │ └── com │ │ │ │ │ └── reactnativetodo │ │ │ │ │ └── ReactNativeFlipper.java │ │ │ └── proguard-rules.pro │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── settings.gradle │ │ ├── build.gradle │ │ └── gradle.properties │ ├── ios │ │ ├── ReactNativeTodo │ │ │ ├── Images.xcassets │ │ │ │ ├── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ ├── AppDelegate.h │ │ │ ├── main.m │ │ │ ├── AppDelegate.mm │ │ │ └── Info.plist │ │ ├── ReactNativeTodo.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ ├── .xcode.env │ │ └── ReactNativeTodoTests │ │ │ └── Info.plist │ ├── .prettierrc.js │ ├── index.js │ ├── Gemfile │ ├── __tests__ │ │ └── App-test.tsx │ ├── metro.config.js │ ├── src │ │ ├── store │ │ │ └── index.ts │ │ ├── App.tsx │ │ └── components │ │ │ ├── Toolbar.tsx │ │ │ └── TodoList.tsx │ ├── README.md │ ├── package.json │ └── .gitignore ├── reduxtagram │ ├── .eslintignore │ ├── src │ │ ├── vite-env.d.ts │ │ ├── styles │ │ │ ├── _variables.css │ │ │ ├── _animations.css │ │ │ └── _typography.css │ │ ├── model │ │ │ ├── index.ts │ │ │ ├── store-model.ts │ │ │ ├── posts-model.ts │ │ │ └── comments-model.ts │ │ ├── assets │ │ │ ├── images │ │ │ │ └── likes-heart.png │ │ │ └── fonts │ │ │ │ ├── billabong-webfont.eot │ │ │ │ ├── billabong-webfont.ttf │ │ │ │ └── billabong-webfont.woff │ │ ├── hooks │ │ │ └── index.ts │ │ ├── index.tsx │ │ ├── store.ts │ │ ├── pages │ │ │ ├── photo-grid.tsx │ │ │ ├── root.tsx │ │ │ └── single.tsx │ │ └── components │ │ │ ├── post-comment.tsx │ │ │ ├── add-comment-form.tsx │ │ │ └── photo.tsx │ ├── public │ │ └── favicon.png │ ├── .prettierrc.json │ ├── .editorconfig │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── .gitignore │ ├── index.html │ ├── tsconfig.json │ ├── test │ │ └── posts-model.spec.ts │ ├── .eslintrc.json │ ├── README.md │ └── package.json ├── kanban │ ├── testSetup.ts │ ├── src │ │ ├── vite-env.d.ts │ │ ├── components │ │ │ ├── TaskList │ │ │ │ ├── index.ts │ │ │ │ ├── TaskList.tsx │ │ │ │ ├── AddTask.test.tsx │ │ │ │ ├── TaskList.test.tsx │ │ │ │ ├── AddTask.tsx │ │ │ │ ├── TaskView.test.tsx │ │ │ │ └── TaskView.tsx │ │ │ └── App.tsx │ │ ├── index.css │ │ ├── utils │ │ │ └── generateId.ts │ │ ├── main.tsx │ │ └── store │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ ├── taskList.model.ts │ │ │ └── model.test.ts │ ├── .eslintignore │ ├── .gitignore │ ├── .prettierignore │ ├── resources │ │ └── kanban-app.gif │ ├── postcss.config.js │ ├── .prettierrc.js │ ├── tsconfig.node.json │ ├── tailwind.config.js │ ├── vite.config.ts │ ├── index.html │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── README.md │ └── package.json ├── simple-todo │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.tsx │ │ ├── store │ │ │ ├── index.ts │ │ │ └── model.ts │ │ └── App.tsx │ ├── .eslintignore │ ├── .gitignore │ ├── .prettierignore │ ├── resources │ │ └── todo-app.gif │ ├── .prettierrc.js │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── index.html │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ └── .eslintrc.js ├── nextjs-todo │ ├── .eslintrc.json │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── next.config.js │ ├── pages │ │ └── _app.tsx │ ├── store │ │ ├── index.ts │ │ └── model.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── nextjs-ssr │ ├── pages │ │ ├── index.js │ │ ├── _app.js │ │ └── ssr.js │ ├── components │ │ ├── Counter.js │ │ ├── Page.js │ │ ├── Shop.js │ │ └── Inventory.js │ ├── .gitignore │ ├── package.json │ ├── README.md │ └── store │ │ └── store.js └── README.md ├── map-set-support.d.ts ├── proxy-polyfill.d.ts ├── tests ├── lib │ └── enable-immer-map-set.js ├── typescript │ ├── index.d.ts │ ├── debug.ts │ ├── react-redux-compat.tsx │ ├── tsconfig.json │ ├── issue87.ts │ ├── issue117.ts │ ├── composed-store.ts │ ├── issue66.tsx │ ├── action.ts │ ├── create-store.ts │ ├── complex-store-inheritance.ts │ ├── issue427.ts │ ├── use-local-store.tsx │ ├── issue246.ts │ ├── external-type-defs.ts │ ├── react.tsx │ ├── persist.tsx │ ├── effect-on.ts │ └── hooks.ts ├── lib.test.js ├── generic.test.js ├── utils.js ├── create-typed-hooks.test.js ├── testing │ ├── README.md │ ├── actions.test.js │ ├── listeners.test.js │ └── react.test.js ├── use-store.test.js ├── dependency-injection.test.js ├── debug.test.js ├── immer.test.js ├── server-rendering.test.js ├── reducer.test.js └── actions.test.js ├── .husky └── pre-commit ├── .yarnrc ├── map-set-support.js ├── proxy-polyfill.js ├── src ├── context.js ├── provider.js ├── constants.js ├── reference.txt ├── actions.js ├── index.js ├── migrations.js ├── create-transform.js ├── use-local-store.js ├── listeners.js ├── create-reducer.js └── helpers.js ├── CHANGELOG.md ├── .github ├── actions │ ├── setup-node │ │ └── action.yml │ └── prepare │ │ └── action.yml ├── workflows │ └── pr-validate.yml └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── .babelrc.js ├── wallaby.js ├── LICENSE └── rollup.config.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | .now -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.ts 3 | *.tsx -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = "https://registry.npmjs.org" -------------------------------------------------------------------------------- /examples/react-native-todo/.node-version: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /examples/react-native-todo/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /examples/react-native-todo/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /map-set-support.d.ts: -------------------------------------------------------------------------------- 1 | export default undefined; 2 | -------------------------------------------------------------------------------- /proxy-polyfill.d.ts: -------------------------------------------------------------------------------- 1 | export default undefined; 2 | -------------------------------------------------------------------------------- /website/docs/releases/README.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | Todo -------------------------------------------------------------------------------- /examples/reduxtagram/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /examples/kanban/testSetup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /examples/kanban/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/simple-todo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/nextjs-todo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/kanban/src/components/TaskList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './TaskList'; 2 | -------------------------------------------------------------------------------- /examples/kanban/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tests/lib/enable-immer-map-set.js: -------------------------------------------------------------------------------- 1 | import { enableMapSet } from 'immer'; 2 | 3 | enableMapSet(); 4 | -------------------------------------------------------------------------------- /examples/kanban/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /examples/kanban/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /examples/react-native-todo/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /examples/react-native-todo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/react-native/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint && yarn test 5 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --install.ignore-engines true 2 | --add.ignore-engines true 3 | --upgrade-interactive.ignore-engines true -------------------------------------------------------------------------------- /examples/kanban/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /examples/simple-todo/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /examples/simple-todo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /map-set-support.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var immer = require('immer'); 4 | 5 | immer.enableMapSet(); 6 | -------------------------------------------------------------------------------- /proxy-polyfill.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var immer = require('immer'); 4 | 5 | immer.enableES5(); 6 | -------------------------------------------------------------------------------- /examples/react-native-todo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactNativeTodo", 3 | "displayName": "ReactNativeTodo" 4 | } -------------------------------------------------------------------------------- /examples/simple-todo/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /website/docs/assets/devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/devtools.png -------------------------------------------------------------------------------- /website/docs/assets/happy-peas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/happy-peas.png -------------------------------------------------------------------------------- /website/docs/assets/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/logo-small.png -------------------------------------------------------------------------------- /website/docs/assets/screencast.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/screencast.mp4 -------------------------------------------------------------------------------- /website/docs/docs/tutorials/extended-api.md: -------------------------------------------------------------------------------- 1 | # Extended API 2 | 3 | Our sincere apologies. These are still being written. 😅 4 | -------------------------------------------------------------------------------- /examples/react-native-todo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | }; 5 | -------------------------------------------------------------------------------- /examples/react-native-todo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /website/docs/assets/devtools-thunk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/devtools-thunk.png -------------------------------------------------------------------------------- /examples/kanban/resources/kanban-app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/kanban/resources/kanban-app.gif -------------------------------------------------------------------------------- /examples/nextjs-todo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/nextjs-todo/public/favicon.ico -------------------------------------------------------------------------------- /examples/reduxtagram/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/reduxtagram/public/favicon.png -------------------------------------------------------------------------------- /website/docs/.vuepress/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/.vuepress/public/favicon.png -------------------------------------------------------------------------------- /website/docs/assets/devtools-action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/devtools-action.png -------------------------------------------------------------------------------- /examples/kanban/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/styles/_variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blue: #125688; 3 | --offwhite: #fafafa; 4 | --lightgrey: #edeeed; 5 | } 6 | -------------------------------------------------------------------------------- /examples/simple-todo/resources/todo-app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/simple-todo/resources/todo-app.gif -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const StoreContext = createContext(); 4 | 5 | export default StoreContext; 6 | -------------------------------------------------------------------------------- /examples/react-native-todo/resources/todo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/resources/todo.gif -------------------------------------------------------------------------------- /examples/reduxtagram/src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './comments-model'; 2 | export * from './posts-model'; 3 | export * from './store-model'; 4 | -------------------------------------------------------------------------------- /tests/typescript/index.d.ts: -------------------------------------------------------------------------------- 1 | // An index.d.t is required for dtslint to work 2 | // Types loaded from the root index.d.ts 3 | // See the tsconfig.json -------------------------------------------------------------------------------- /website/docs/.vuepress/public/happy-peas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/.vuepress/public/happy-peas.png -------------------------------------------------------------------------------- /website/docs/assets/devtools-listenaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/devtools-listenaction.png -------------------------------------------------------------------------------- /website/docs/assets/devtools-listenthunk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/devtools-listenthunk.png -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/debug.keystore -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ReactNativeTodo 3 | 4 | -------------------------------------------------------------------------------- /examples/react-native-todo/ios/ReactNativeTodo/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/assets/images/likes-heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/reduxtagram/src/assets/images/likes-heart.png -------------------------------------------------------------------------------- /website/docs/assets/typescript-tutorial/typed-hooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/typescript-tutorial/typed-hooks.png -------------------------------------------------------------------------------- /website/docs/assets/typescript-tutorial/typing-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/typescript-tutorial/typing-model.png -------------------------------------------------------------------------------- /examples/reduxtagram/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "endOfLine": "auto" 7 | } 8 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/assets/fonts/billabong-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/reduxtagram/src/assets/fonts/billabong-webfont.eot -------------------------------------------------------------------------------- /examples/reduxtagram/src/assets/fonts/billabong-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/reduxtagram/src/assets/fonts/billabong-webfont.ttf -------------------------------------------------------------------------------- /examples/reduxtagram/src/assets/fonts/billabong-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/reduxtagram/src/assets/fonts/billabong-webfont.woff -------------------------------------------------------------------------------- /website/docs/assets/typescript-tutorial/typed-action-imp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/typescript-tutorial/typed-action-imp.png -------------------------------------------------------------------------------- /website/docs/assets/typescript-tutorial/typed-get-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/typescript-tutorial/typed-get-state.png -------------------------------------------------------------------------------- /website/docs/assets/typescript-tutorial/typed-thunk-imp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/typescript-tutorial/typed-thunk-imp.png -------------------------------------------------------------------------------- /examples/react-native-todo/ios/ReactNativeTodo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /website/docs/assets/typescript-tutorial/typed-computed-imp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/typescript-tutorial/typed-computed-imp.png -------------------------------------------------------------------------------- /website/docs/assets/typescript-tutorial/typed-thunk-globals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/typescript-tutorial/typed-thunk-globals.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | We follow semantic versioning. 2 | 3 | See the [releases](https://github.com/ctrlplusb/easy-peasy/releases) page on 4 | GitHub for information regarding each release. 5 | -------------------------------------------------------------------------------- /website/docs/assets/typescript-tutorial/typed-action-dispatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/typescript-tutorial/typed-action-dispatch.png -------------------------------------------------------------------------------- /website/docs/assets/typescript-tutorial/typed-injections-imp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/typescript-tutorial/typed-injections-imp.png -------------------------------------------------------------------------------- /website/docs/assets/typescript-tutorial/typed-thunk-dispatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/website/docs/assets/typescript-tutorial/typed-thunk-dispatch.png -------------------------------------------------------------------------------- /examples/react-native-todo/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/kanban/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 90, 6 | tabWidth: 2, 7 | endOfLine: 'auto', 8 | }; 9 | -------------------------------------------------------------------------------- /examples/nextjs-ssr/pages/index.js: -------------------------------------------------------------------------------- 1 | import Page from "../components/Page"; 2 | 3 | function Index() { 4 | return ; 5 | } 6 | 7 | export default Index; 8 | -------------------------------------------------------------------------------- /tests/lib.test.js: -------------------------------------------------------------------------------- 1 | import { get } from '../src/lib'; 2 | 3 | describe('get', () => { 4 | test('invalid target', () => { 5 | expect(get(['foo'], 12345)).toBeUndefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /examples/kanban/src/utils/generateId.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/8084248/1980235 2 | const generateId = () => (Math.random() + 1).toString(36).substring(7); 3 | 4 | export default generateId; 5 | -------------------------------------------------------------------------------- /examples/simple-todo/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 90, 6 | tabWidth: 2, 7 | endOfLine: 'auto', 8 | }; 9 | -------------------------------------------------------------------------------- /examples/kanban/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/react-native-todo/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: false, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /examples/simple-todo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /website/docs/docs/known-issues/README.md: -------------------------------------------------------------------------------- 1 | # Known Issues 2 | 3 | This is a collection of known issues. These may exist due to 3rd party library restrictions are issues with the current architectural design of Easy Peasy. -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctrlplusb/easy-peasy/HEAD/examples/react-native-todo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/simple-todo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /website/docs/docs/api/action-on.md: -------------------------------------------------------------------------------- 1 | # actionOn 2 | 3 | Allows you to declare an action which gets executed when configured target actions are executed. 4 | 5 | For more information please see the [listeners](/docs/api/listeners.html) documentation. -------------------------------------------------------------------------------- /website/docs/docs/api/generic.md: -------------------------------------------------------------------------------- 1 | # generic 2 | 3 | This helper is useful in the context of using generics within your TypeScript models. 4 | 5 | Please see the [`Generic`](/docs/typescript-api/generic.html) type documentation for more information. -------------------------------------------------------------------------------- /website/docs/docs/api/thunk-on.md: -------------------------------------------------------------------------------- 1 | # thunkOn 2 | 3 | Allows you to declare a thunk which gets executed when configured target actions are executed. 4 | 5 | For more information please see the [listeners](/docs/api/listeners.html) documentation. -------------------------------------------------------------------------------- /examples/reduxtagram/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | max_line_length = 120 11 | -------------------------------------------------------------------------------- /examples/kanban/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require('@tailwindcss/forms')], 8 | }; 9 | -------------------------------------------------------------------------------- /examples/react-native-todo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './src/App'; 7 | import {name as appName} from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /examples/nextjs-todo/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | eslint: { 6 | ignoreDuringBuilds: true, 7 | }, 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /examples/react-native-todo/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby File.read(File.join(__dir__, '.ruby-version')).strip 5 | 6 | gem 'cocoapods', '~> 1.11', '>= 1.11.3' 7 | -------------------------------------------------------------------------------- /examples/reduxtagram/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/react-native-todo/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /examples/react-native-todo/ios/ReactNativeTodo/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/actions/setup-node/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Node.js 2 | description: A composite action to setup the required Node.js version. 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: actions/setup-node@v3 8 | with: 9 | node-version: 22.x 10 | cache: 'yarn' 11 | -------------------------------------------------------------------------------- /examples/react-native-todo/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ReactNativeTodo' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/react-native-gradle-plugin') 5 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { createTypedHooks } from 'easy-peasy'; 2 | import { StoreModel } from '@/model'; 3 | 4 | const { useStoreActions, useStoreDispatch, useStoreState, useStore } = createTypedHooks(); 5 | 6 | export { useStoreActions, useStoreDispatch, useStoreState, useStore }; 7 | -------------------------------------------------------------------------------- /src/provider.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types,react/no-children-prop */ 2 | 3 | import React from 'react'; 4 | import StoreContext from './context'; 5 | 6 | export function StoreProvider({ children, store }) { 7 | return ( 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const actionSymbol = '$_a'; 2 | export const actionOnSymbol = '$_aO'; 3 | export const computedSymbol = '$_c'; 4 | export const effectOnSymbol = '$_e'; 5 | export const persistSymbol = '$_p'; 6 | export const reducerSymbol = '$_r'; 7 | export const thunkOnSymbol = '$_tO'; 8 | export const thunkSymbol = '$_t'; 9 | -------------------------------------------------------------------------------- /tests/generic.test.js: -------------------------------------------------------------------------------- 1 | import { createStore, generic } from '../src'; 2 | 3 | test('generic values are passed through as state', () => { 4 | // ACT 5 | const store = createStore({ 6 | foo: generic(1337), 7 | }); 8 | 9 | // ASSERT 10 | expect(store.getState()).toEqual({ 11 | foo: 1337, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /.github/actions/prepare/action.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Application 2 | description: A composite action to prepare the library. 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: ./.github/actions/setup-node 8 | 9 | - name: Install Dependencies 10 | shell: bash 11 | run: yarn install --frozen-lockfile 12 | -------------------------------------------------------------------------------- /tests/typescript/debug.ts: -------------------------------------------------------------------------------- 1 | import { debug, action, Action } from 'easy-peasy'; 2 | 3 | interface Model { 4 | logs: string[]; 5 | add: Action; 6 | } 7 | 8 | const model: Model = { 9 | logs: [], 10 | add: action((state, payload) => { 11 | const foo = debug(state); 12 | console.log(foo); 13 | }), 14 | }; 15 | -------------------------------------------------------------------------------- /examples/react-native-todo/ios/ReactNativeTodo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import Root from '@/pages/root'; 4 | import '@/styles/index.css'; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')!); 7 | 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /examples/nextjs-todo/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import { StoreProvider } from 'easy-peasy'; 3 | import store from '../store'; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return 7 | 8 | 9 | } 10 | 11 | export default MyApp 12 | -------------------------------------------------------------------------------- /examples/reduxtagram/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | import eslintPlugin from 'vite-plugin-eslint'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(), tsconfigPaths(), eslintPlugin()], 9 | }); 10 | -------------------------------------------------------------------------------- /website/docs/docs/api/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # API 6 | 7 | This sections provides you with a full overview of the API provided by Easy Peasy. Even if you have been through the tutorial we highly recommend that you review this section as the tutorial does not cover the full API surface area as well as all the features available via each helper. 8 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "engines": { 4 | "node": "14.x" 5 | }, 6 | "scripts": { 7 | "dev": "vuepress dev docs", 8 | "build": "vuepress build docs", 9 | "deploy": "vercel --prod" 10 | }, 11 | "devDependencies": { 12 | "@vuepress/plugin-google-analytics": "^1.9.9", 13 | "vuepress": "^1.9.9" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/react-native-todo/__tests__/App-test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/model/store-model.ts: -------------------------------------------------------------------------------- 1 | import { postsModel, PostsModel } from './posts-model'; 2 | import { commentsModel, CommentsModel } from './comments-model'; 3 | 4 | export interface StoreModel { 5 | commentsModel: CommentsModel; 6 | postsModel: PostsModel; 7 | } 8 | 9 | export const storeModel: StoreModel = { 10 | commentsModel, 11 | postsModel, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/nextjs-ssr/pages/_app.js: -------------------------------------------------------------------------------- 1 | import { StoreProvider } from 'easy-peasy'; 2 | import { useStore } from '../store/store'; 3 | 4 | export default function WrappedApp({ Component, pageProps }) { 5 | const store = useStore(pageProps.serverStoreState); 6 | 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/reduxtagram/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | /node_modules 11 | /dist 12 | dist-ssr 13 | *.local 14 | /coverage 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /examples/react-native-todo/metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: true, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Dependencies 9 | node_modules 10 | 11 | # Debug log from npm 12 | npm-debug.log 13 | 14 | # Jest 15 | coverage 16 | 17 | # Build output 18 | dist 19 | .size-snapshot.json 20 | 21 | # Flow 22 | flow-coverage 23 | flow-typed 24 | 25 | # Yarn 26 | yarn-error.log 27 | 28 | # Temp directories 29 | temp 30 | 31 | # IDEs 32 | .vscode 33 | -------------------------------------------------------------------------------- /src/reference.txt: -------------------------------------------------------------------------------- 1 | Internal Variable Reference 2 | 3 | _r = _references 4 | _r._i = _r._internals 5 | 6 | _aRD = _actionReducersDict 7 | _cR = _customReducers 8 | _cP = _computedProperties 9 | _dS = _defaultState 10 | _aCD = _actionCreatorDict 11 | _aC = _actionCreators 12 | _e = _effects 13 | _lAC = _listenerActionCreators 14 | _lAM = _listenerActionMap 15 | _cS = _computedState 16 | 17 | -------------------------------------------------------------------------------- /examples/kanban/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import react from '@vitejs/plugin-react'; 5 | import { defineConfig } from 'vite'; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [react()], 10 | test: { 11 | globals: true, 12 | environment: 'jsdom', 13 | setupFiles: './testSetup.ts', 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /tests/utils.js: -------------------------------------------------------------------------------- 1 | const defaultKeys = ['log', 'warn', 'error']; 2 | 3 | export const mockConsole = (methodsToMock = defaultKeys) => { 4 | const originalConsole = { ...console }; 5 | methodsToMock.forEach((key) => { 6 | global.console[key] = jest.fn(); 7 | }); 8 | // Return function to restore console 9 | const restore = () => { 10 | global.console = originalConsole; 11 | }; 12 | return restore; 13 | }; 14 | -------------------------------------------------------------------------------- /website/docs/docs/recipes/README.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | This section contains a set of recipes for some common requests around configuring Easy Peasy or integrating it with other libraries. 4 | 5 | For a full setup, look through the [examples using Easy Peasy on GitHub](https://github.com/ctrlplusb/easy-peasy/tree/master/examples). 6 | 7 | If you have any recipes of your own we would invite you to submit a PR to get it added here. 8 | 9 | -------------------------------------------------------------------------------- /website/docs/docs/typescript-api/README.md: -------------------------------------------------------------------------------- 1 | # TypeScript API 2 | 3 | This section provides you with an overview of the primary types you are likely to use from Easy Peasy. 4 | 5 | Generally you only need to explicitly import and use the types when defining your model interfaces. 6 | 7 | If you are unfamiliar with using Easy Peasy with TypeScript then we would recommend that you read the [TypeScript tutorial](/docs/tutorials/typescript.html). 8 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'easy-peasy'; 2 | import comments from '@/data/comments'; 3 | import posts from '@/data/posts'; 4 | import { storeModel } from '@/model'; 5 | 6 | const store = createStore(storeModel, { 7 | devTools: import.meta.env.DEV ? { name: 'Easy Peasy Reduxtagram' } : false, 8 | initialState: { commentsModel: { comments }, postsModel: { posts } }, 9 | }); 10 | 11 | export default store; 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | yarn: true 5 | directories: 6 | - node_modules 7 | node_js: 8 | - '12' 9 | script: 10 | # Unfortunately flow falls over when a dep exists in peer deps and others. :( 11 | # @see https://github.com/flowtype/flow-typed/issues/528 12 | #- yarn run flow:defs 13 | - yarn run test 14 | after_success: 15 | # Deploy code coverage report to codecov.io 16 | - yarn run test:coverage:deploy 17 | -------------------------------------------------------------------------------- /examples/kanban/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Easy-peasy kanban 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/simple-todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Easy-peasy todos 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/simple-todo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StoreProvider } from 'easy-peasy'; 2 | import React from 'react'; 3 | import { createRoot } from 'react-dom/client'; 4 | 5 | import App from './App'; 6 | import store from './store'; 7 | 8 | const root = createRoot(document.getElementById('root')!); 9 | 10 | root.render( 11 | 12 | 13 | 14 | 15 | , 16 | ); 17 | -------------------------------------------------------------------------------- /tests/typescript/react-redux-compat.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { createStore, StoreProvider } from 'easy-peasy'; 4 | 5 | interface StoreModel { 6 | foo: string; 7 | } 8 | 9 | const store = createStore({ 10 | foo: 'bar', 11 | }); 12 | 13 | const app = ( 14 | 15 | 16 |
17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /website/docs/docs/introduction/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Firstly, you'll need to make sure that you have React and React DOM installed. 4 | Please ensure you install versions >= 16.8.0 for both `react` and `react-dom`, as 5 | this library depends on the hooks feature of React. 6 | 7 | ```bash 8 | npm install react 9 | npm install react-dom 10 | ``` 11 | 12 | Then install Easy Peasy. 13 | 14 | ```bash 15 | npm install easy-peasy 16 | ``` 17 | 18 | We're off to the races! 19 | -------------------------------------------------------------------------------- /examples/nextjs-todo/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, createTypedHooks } from 'easy-peasy'; 2 | import todosStore, { TodosModel } from './model'; 3 | 4 | const store = createStore(todosStore); 5 | 6 | const typedHooks = createTypedHooks(); 7 | 8 | export const useStoreActions = typedHooks.useStoreActions; 9 | export const useStoreDispatch = typedHooks.useStoreDispatch; 10 | export const useStoreState = typedHooks.useStoreState; 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /examples/simple-todo/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, createTypedHooks } from 'easy-peasy'; 2 | import todosStore, { TodosModel } from './model'; 3 | 4 | const store = createStore(todosStore); 5 | 6 | const typedHooks = createTypedHooks(); 7 | 8 | export const useStoreActions = typedHooks.useStoreActions; 9 | export const useStoreDispatch = typedHooks.useStoreDispatch; 10 | export const useStoreState = typedHooks.useStoreState; 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /tests/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2017", "dom"], 5 | "jsx": "react", 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "strictFunctionTypes": true, 9 | "strictNullChecks": true, 10 | "types": [], 11 | "noEmit": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "baseUrl": "../..", 14 | "paths": { 15 | "easy-peasy": ["index.d.ts"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/react-native-todo/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import {createStore, createTypedHooks} from 'easy-peasy'; 2 | import todosStore, {TodosModel} from './model'; 3 | 4 | const store = createStore(todosStore); 5 | 6 | const typedHooks = createTypedHooks(); 7 | 8 | export const useStoreActions = typedHooks.useStoreActions; 9 | export const useStoreDispatch = typedHooks.useStoreDispatch; 10 | export const useStoreState = typedHooks.useStoreState; 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /examples/kanban/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StoreProvider } from 'easy-peasy'; 2 | import { StrictMode } from 'react'; 3 | import { createRoot } from 'react-dom/client'; 4 | 5 | import App from './components/App'; 6 | import store from './store'; 7 | 8 | import './index.css'; 9 | 10 | const root = createRoot(document.getElementById('root') as HTMLElement); 11 | root.render( 12 | 13 | 14 | 15 | 16 | , 17 | ); 18 | -------------------------------------------------------------------------------- /examples/nextjs-ssr/pages/ssr.js: -------------------------------------------------------------------------------- 1 | import Page from '../components/Page'; 2 | 3 | import { initializeStore } from '../store/store'; 4 | 5 | export default function SSR() { 6 | return ; 7 | } 8 | 9 | export function getServerSideProps() { 10 | const store = initializeStore(); 11 | 12 | const data = ['apple', 'pear', 'orange', 'nuts']; 13 | store.getActions().inventory.setItems(data); 14 | 15 | return { props: { serverStoreState: store.getState() } }; 16 | } 17 | -------------------------------------------------------------------------------- /examples/nextjs-ssr/components/Counter.js: -------------------------------------------------------------------------------- 1 | import { useStoreState, useStoreActions } from "easy-peasy"; 2 | 3 | export default function Counter() { 4 | const count = useStoreState((state) => state.counter.count); 5 | const increment = useStoreActions((actions) => actions.counter.increment); 6 | 7 | return ( 8 |
9 |

Counter

10 |

value = {count}

11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/pr-validate.yml: -------------------------------------------------------------------------------- 1 | name: PR Validate 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | code-quality: 10 | name: Code Quality 11 | strategy: 12 | matrix: 13 | command: 14 | - lint 15 | - dtslint 16 | - test:coverage 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: ./.github/actions/prepare 21 | - shell: bash 22 | run: yarn ${{ matrix.command }} 23 | -------------------------------------------------------------------------------- /examples/kanban/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, createTypedHooks, persist } from 'easy-peasy'; 2 | import storeDefinition, { StoreModel } from './model'; 3 | 4 | const store = createStore(persist(storeDefinition)); 5 | 6 | const typedHooks = createTypedHooks(); 7 | 8 | export const useStoreActions = typedHooks.useStoreActions; 9 | export const useStoreDispatch = typedHooks.useStoreDispatch; 10 | export const useStoreState = typedHooks.useStoreState; 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /tests/typescript/issue87.ts: -------------------------------------------------------------------------------- 1 | import { action, Action, createStore } from 'easy-peasy'; 2 | 3 | interface IAnimal { 4 | name: string; 5 | age?: number; 6 | } 7 | 8 | interface IModel { 9 | animal: IAnimal; 10 | setAnimal: Action; 11 | } 12 | 13 | const model: IModel = { 14 | animal: { 15 | name: 'robert', 16 | }, 17 | setAnimal: action((state, payload) => { 18 | return { ...state, animal: payload.animal }; 19 | }), 20 | }; 21 | 22 | const store = createStore(model); 23 | -------------------------------------------------------------------------------- /examples/kanban/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import TaskList from './TaskList'; 2 | 3 | const App = () => { 4 | return ( 5 |
6 |
7 | 8 | 9 | 10 |
11 | 12 |

13 | Data is persisted in the `sessionStorage`. 14 |

15 |
16 | ); 17 | }; 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/styles/_animations.css: -------------------------------------------------------------------------------- 1 | .likes-heart { 2 | opacity: 0; 3 | transition: all 0.5s; 4 | transform: translateX(-50%) translateY(-50%) scale(5); 5 | display: block; 6 | } 7 | 8 | .likes-heart.like-enter { 9 | transition: all 0.2s; 10 | transform: translateX(-50%) translateY(-50%) scale(1); 11 | opacity: 1; 12 | } 13 | 14 | .likes-heart.like-enter.like-enter-active { 15 | transform: translateX(-50%) translateY(-50%) scale(5); 16 | } 17 | 18 | .likes-heart .like-exit-active { 19 | display: none; 20 | } 21 | -------------------------------------------------------------------------------- /tests/create-typed-hooks.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | createTypedHooks, 3 | useStoreActions, 4 | useStoreDispatch, 5 | useStoreState, 6 | useStore, 7 | } from '../src'; 8 | 9 | test('exports all hooks', () => { 10 | // ACT 11 | const typedHooks = createTypedHooks(); 12 | 13 | // ASSERT 14 | expect(typedHooks.useStoreActions).toBe(useStoreActions); 15 | expect(typedHooks.useStoreState).toBe(useStoreState); 16 | expect(typedHooks.useStoreDispatch).toBe(useStoreDispatch); 17 | expect(typedHooks.useStore).toBe(useStore); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/nextjs-ssr/components/Page.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import Counter from './Counter'; 4 | import Shop from './Shop'; 5 | import Inventory from './Inventory'; 6 | 7 | function Page({ title, linkTo }) { 8 | return ( 9 |
10 |

{title}

11 | 12 | 13 | 14 | {linkTo === '/' ? '/index' : linkTo} 15 | 16 | {' - '} 17 | SSR (again) 18 |
19 | ); 20 | } 21 | 22 | export default Page; 23 | -------------------------------------------------------------------------------- /examples/reduxtagram/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Reduxtagram 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/react-native-todo/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /tests/testing/README.md: -------------------------------------------------------------------------------- 1 | # Testing Showcase 2 | 3 | This test suite showcases different strategies for testing your `easy-peasy` 4 | store as well as components that are consuming your store. 5 | 6 | When testing your models, we recommend testing each model slice in isolation 7 | rather than testing the entire store. You can import your model under test 8 | and in the "arrange/before" of your test you could use the `createStore` API 9 | providing just your model under test. This helps to isolate your tests and 10 | make them far less vulnerable to code refactoring. -------------------------------------------------------------------------------- /examples/nextjs-ssr/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /examples/nextjs-todo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/nextjs-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-peasy-next-js-ssr", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "next", 6 | "build": "next build", 7 | "start": "next start" 8 | }, 9 | "dependencies": { 10 | "easy-peasy": "^6.0.0", 11 | "next": "15.1.7", 12 | "react": "19.0.0", 13 | "react-dom": "19.0.0", 14 | "react-redux": "9.2.0", 15 | "redux": "5.0.1", 16 | "redux-devtools-extension": "2.13.9", 17 | "redux-thunk": "3.1.0" 18 | }, 19 | "license": "MIT", 20 | "keywords": [], 21 | "description": "" 22 | } 23 | -------------------------------------------------------------------------------- /tests/use-store.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import { createStore, StoreProvider, useStore } from '../src'; 5 | 6 | test('returns the store instance', () => { 7 | // ARRANGE 8 | const store = createStore({ 9 | foo: 'bar', 10 | }); 11 | 12 | function Consumer() { 13 | const actual = useStore(); 14 | expect(actual).toBe(store); 15 | return null; 16 | } 17 | 18 | // ACT 19 | render( 20 | 21 | 22 | , 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/nextjs-ssr/README.md: -------------------------------------------------------------------------------- 1 | # Simple SSR example with Next.js 2 | 3 | This is a [Next.js](https://nextjs.org/) example based on 4 | [this codesandbox](https://codesandbox.io/s/c24rg). 5 | 6 | ## Getting Started 7 | 8 | First, run the development server: 9 | 10 | ```bash 11 | yarn dev 12 | ``` 13 | 14 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the 15 | result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page 18 | auto-updates as you edit the file. 19 | 20 | The `easy-peasy` store & models are located in the `store` folder. 21 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/model/posts-model.ts: -------------------------------------------------------------------------------- 1 | import { Action, action } from 'easy-peasy'; 2 | 3 | export interface Post { 4 | id: string; 5 | caption: string; 6 | likes: number; 7 | src: string; 8 | } 9 | 10 | export interface PostsModel { 11 | posts: Post[]; 12 | 13 | likePost: Action; 14 | } 15 | 16 | export const postsModel: PostsModel = { 17 | posts: [], 18 | 19 | likePost: action((state, postId) => { 20 | const post = state.posts.find((item) => item.id === postId); 21 | 22 | if (post) { 23 | post.likes += 1; 24 | } 25 | }), 26 | }; 27 | -------------------------------------------------------------------------------- /examples/nextjs-ssr/components/Shop.js: -------------------------------------------------------------------------------- 1 | import { useStoreState, useStoreRehydrated } from 'easy-peasy' 2 | 3 | export default function Shop() { 4 | const basket = useStoreState(state => state.shop.basket) 5 | const isRehydrated = useStoreRehydrated() 6 | 7 | return ( 8 |
9 |

Basket

10 |
    11 | {isRehydrated && Object.entries(basket).map(([item, quantity], index) => 12 |
  • {item} x {quantity}
  • 13 | )} 14 |
15 |
16 | ) 17 | } -------------------------------------------------------------------------------- /tests/dependency-injection.test.js: -------------------------------------------------------------------------------- 1 | import { createStore, thunk } from '../src'; 2 | 3 | test('exposes dependencies to effect actions', async () => { 4 | // ARRANGE 5 | const injection = jest.fn(); 6 | const store = createStore( 7 | { 8 | doSomething: thunk((actions, payload, { injections }) => { 9 | injections.injection(); 10 | }), 11 | }, 12 | { 13 | injections: { 14 | injection, 15 | }, 16 | }, 17 | ); 18 | 19 | // ACT 20 | await store.getActions().doSomething(); 21 | 22 | // ASSERT 23 | expect(injection).toHaveBeenCalledTimes(1); 24 | }); 25 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { actionOnSymbol } from './constants'; 2 | 3 | export function createActionCreator(def, _r) { 4 | function actionCreator(payload) { 5 | const action = { 6 | type: def.meta.type, 7 | payload, 8 | config: def.config, 9 | }; 10 | if (def[actionOnSymbol] && def.meta.resolvedTargets) { 11 | payload.resolvedTargets = [...def.meta.resolvedTargets]; 12 | } 13 | return _r.dispatch(action); 14 | } 15 | 16 | // We bind the types to the creator for easy reference by consumers 17 | actionCreator.type = def.meta.type; 18 | 19 | return actionCreator; 20 | } 21 | -------------------------------------------------------------------------------- /examples/nextjs-todo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # [ctrlplusb] 4 | patreon: # ctrlplusb 5 | open_collective: easy-peasy 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /examples/react-native-todo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {SafeAreaView, StyleSheet} from 'react-native'; 3 | 4 | import {StoreProvider} from 'easy-peasy'; 5 | import store from './store'; 6 | import {TodoList} from './components/TodoList'; 7 | 8 | function App(): JSX.Element { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | export const styles = StyleSheet.create({ 19 | mainView: {backgroundColor: '#FFF4C2', flex: 1}, 20 | }); 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/pages/photo-grid.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Photo from '@/components/photo'; 3 | import { useStoreState } from '@/hooks'; 4 | 5 | export default function PhotoGrid() { 6 | const allPosts = useStoreState((state) => state.postsModel.posts); 7 | const allComments = useStoreState((state) => state.commentsModel.comments); 8 | 9 | return ( 10 |
11 | {allPosts.map((post) => { 12 | const postComments = allComments[post.id]; 13 | return ; 14 | })} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /website/docs/docs/api/store-provider.md: -------------------------------------------------------------------------------- 1 | # StoreProvider 2 | 3 | This component is responsible for exposing your [store](/docs/api/store.html) to 4 | your React application. This ensures that any of the store hooks within your 5 | application have access to the store. 6 | 7 | Really important stuff here. :) 8 | 9 | ## Example 10 | 11 | ```javascript 12 | import { StoreProvider, createStore } from 'easy-peasy'; 13 | import model from './model'; 14 | 15 | const store = createStore(model); 16 | 17 | function App() { 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /website/docs/docs/api/use-store.md: -------------------------------------------------------------------------------- 1 | # useStore 2 | 3 | A [hook](https://reactjs.org/docs/hooks-intro.html) granting your components access to the [store](/docs/api/store.html) instance. 4 | 5 | > This should only be used for advanced or exceptional cases, for e.g. when you would like to dynamically extend the store deep within your component tree. 6 | 7 | ```javascript 8 | const store = useStore(); 9 | ``` 10 | 11 | ## Example 12 | 13 | ```javascript 14 | import { useStore } from 'easy-peasy'; 15 | 16 | const AddTodo = () => { 17 | const store = useStore(); 18 | return ( 19 |
20 | {store.getState().sayHello} 21 |
22 | ); 23 | }; 24 | ``` -------------------------------------------------------------------------------- /examples/kanban/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples with `easy-peasy` 2 | 3 | - [Simple todo app](./simple-todo/) [vite, react, TS] 4 | ([View codesandbox](https://codesandbox.io/s/fnidh1)) 5 | - [Simple todo app (Next)](./nextjs-ssr/) [Next.js, JS] 6 | - [Simple todo app (Next)](./nextjs-todo/) [Next.js, TS] 7 | - [Kanban](./kanban/) [vite, react, TS, vitest, testing-library] 8 | ([View codesandbox](https://codesandbox.io/s/5zdk6r)) 9 | - [React Native todo app](./react-native-todo/) 10 | - [Reduxstagram](./reduxtagram/) 11 | ([View sandbox](https://codesandbox.io/s/ztuxzk)) 12 | 13 | ## Missing an example? 14 | 15 | Create a PR with your example OR Create an issue of what you want to see 16 | -------------------------------------------------------------------------------- /examples/nextjs-todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-peasy-example-nextjs-todo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "easy-peasy": "^6.0.0", 13 | "next": "15.1.7", 14 | "react": "19.0.0", 15 | "react-dom": "19.0.0" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "18.7.18", 19 | "@types/react": "19.0.9", 20 | "@types/react-dom": "19.0.3", 21 | "eslint": "8.23.1", 22 | "eslint-config-next": "12.3.0", 23 | "typescript": "4.8.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/simple-todo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /tests/typescript/issue117.ts: -------------------------------------------------------------------------------- 1 | import { Action, action } from 'easy-peasy'; 2 | 3 | interface Notification { 4 | id: string; 5 | title: string; 6 | subTitle: string; 7 | } 8 | 9 | interface Notifications { 10 | [key: string]: Notification; 11 | } 12 | 13 | export interface NotificationsModel { 14 | items: Notifications; 15 | add: Action; 16 | } 17 | 18 | let id = 1; 19 | 20 | const notificationsModel: NotificationsModel = { 21 | items: {}, 22 | add: action((state, payload) => { 23 | id += 1; 24 | // 👇 error 25 | state.items[id.toString()] = payload; 26 | }), 27 | }; 28 | 29 | export default notificationsModel; 30 | -------------------------------------------------------------------------------- /examples/react-native-todo/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "33.0.0" 6 | minSdkVersion = 21 7 | compileSdkVersion = 33 8 | targetSdkVersion = 33 9 | 10 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. 11 | ndkVersion = "23.1.7779620" 12 | } 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | dependencies { 18 | classpath("com.android.tools.build:gradle:7.3.1") 19 | classpath("com.facebook.react:react-native-gradle-plugin") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/nextjs-ssr/components/Inventory.js: -------------------------------------------------------------------------------- 1 | import { useStoreState, useStoreActions } from 'easy-peasy' 2 | 3 | export default function Inventory() { 4 | const items = useStoreState(state => state.inventory.items) 5 | const addToBasket = useStoreActions(actions => actions.shop.addItem) 6 | 7 | return ( 8 |
9 |

Items

10 |
    11 | {items.map((item, index) => 12 |
  • 13 | {item} 14 | 15 |
  • 16 | )} 17 |
18 |
19 | ) 20 | } -------------------------------------------------------------------------------- /tests/typescript/composed-store.ts: -------------------------------------------------------------------------------- 1 | import { thunk, Thunk } from 'easy-peasy'; 2 | 3 | type Injections = any; 4 | 5 | interface IModelActions { 6 | someThunk: Thunk; 7 | } 8 | 9 | interface IModel extends IModelActions { 10 | someValue: T; 11 | } 12 | 13 | interface IStoreModel { 14 | a: IModel; 15 | b: IModel; 16 | } 17 | 18 | const createModelActions = (): IModelActions => { 19 | return { 20 | someThunk: thunk(() => {}), 21 | }; 22 | }; 23 | 24 | const storeModel: IStoreModel = { 25 | a: { 26 | someValue: 'hello', 27 | ...createModelActions(), 28 | }, 29 | b: { 30 | someValue: 123, 31 | ...createModelActions(), 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /tests/typescript/issue66.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { action, Action, createStore } from 'easy-peasy'; 3 | import { createRoot } from 'react-dom/client'; 4 | 5 | interface CartModel { 6 | products?: string[] | null; 7 | setProducts: Action; 8 | } 9 | 10 | interface Model { 11 | cart: CartModel; 12 | } 13 | 14 | const model: Model = { 15 | cart: { 16 | products: null, 17 | setProducts: action((state, payload) => { 18 | state.products = payload; 19 | }), 20 | }, 21 | }; 22 | 23 | const store = createStore(model); 24 | 25 | const App = () => { 26 | return

test

; 27 | }; 28 | 29 | const root = createRoot(document.getElementById('app')!); 30 | root.render(); 31 | -------------------------------------------------------------------------------- /website/docs/docs/api/use-store-dispatch.md: -------------------------------------------------------------------------------- 1 | # useStoreDispatch 2 | 3 | A [hook](https://reactjs.org/docs/hooks-intro.html) granting your components access to the [store's](/docs/api/store.html) dispatch. 4 | 5 | ```javascript 6 | const dispatch = useStoreDispatch(); 7 | ``` 8 | 9 | ## Example 10 | 11 | ```javascript 12 | import { useState } from 'react'; 13 | import { useStoreDispatch } from 'easy-peasy'; 14 | 15 | const AddTodo = () => { 16 | const [text, setText] = useState(''); 17 | const dispatch = useStoreDispatch(); 18 | return ( 19 |
20 | setText(e.target.value)} /> 21 | 22 |
23 | ); 24 | }; 25 | ``` -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV } = process.env; 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | '@babel/env', 7 | { 8 | targets: 9 | NODE_ENV === 'test' 10 | ? { node: 12 } 11 | : { 12 | browsers: ['ie >= 11'], 13 | }, 14 | exclude: ['transform-async-to-generator', 'transform-regenerator'], 15 | modules: false, 16 | loose: true, 17 | }, 18 | ], 19 | ], 20 | plugins: [ 21 | '@babel/plugin-transform-react-jsx', 22 | // don't use `loose` mode here - need to copy symbols when spreading 23 | '@babel/proposal-object-rest-spread', 24 | NODE_ENV === 'test' && '@babel/transform-modules-commonjs', 25 | ].filter(Boolean), 26 | }; 27 | -------------------------------------------------------------------------------- /examples/kanban/src/store/model.ts: -------------------------------------------------------------------------------- 1 | import generateId from '../utils/generateId'; 2 | import createTaskListStore, { TaskListModel } from './taskList.model'; 3 | 4 | export interface StoreModel { 5 | todo: TaskListModel; 6 | doing: TaskListModel; 7 | done: TaskListModel; 8 | } 9 | 10 | const store: StoreModel = { 11 | todo: createTaskListStore({ 12 | name: 'Todo', 13 | tasks: [{ id: generateId(), name: 'Explore easy-peasy' }], 14 | progressTasksTo: 'doing', 15 | }), 16 | doing: createTaskListStore({ 17 | name: 'Doing', 18 | tasks: [], 19 | regressTasksTo: 'todo', 20 | progressTasksTo: 'done', 21 | }), 22 | done: createTaskListStore({ name: 'Done', tasks: [], regressTasksTo: 'doing' }), 23 | }; 24 | 25 | export default store; 26 | -------------------------------------------------------------------------------- /examples/reduxtagram/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "allowJs": false, 8 | "skipLibCheck": true, 9 | "esModuleInterop": false, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "ESNext", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | "paths": { 20 | "@/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["src", "test"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/components/post-comment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useStoreActions } from '@/hooks'; 3 | import { Comment } from '@/model'; 4 | 5 | interface Props { 6 | postId: string; 7 | comment: Comment; // comment related to post 8 | } 9 | 10 | export default function PostComment({ postId, comment }: Props) { 11 | const removeComment = useStoreActions((actions) => actions.commentsModel.removeComment); 12 | 13 | return ( 14 |
15 |

16 | {comment.user} 17 | {comment.text} 18 | 21 |

22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /tests/testing/actions.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These tests show how you can test actions. They are probably the most simple 3 | * of tests as actions are merely an update to the store state. Therefore to 4 | * test an action you can simply fire it and then assert against the expected 5 | * state of your store. 6 | */ 7 | 8 | import { action, createStore } from '../../src'; 9 | 10 | const todosModel = { 11 | items: {}, 12 | add: action((state, payload) => { 13 | state.items[payload.id] = payload; 14 | }), 15 | }; 16 | 17 | it('state gets updated', () => { 18 | // ARRANGE 19 | const todo = { id: 1, text: 'foo' }; 20 | const store = createStore(todosModel); 21 | 22 | // ACT 23 | store.getActions().add(todo); 24 | 25 | // ASSERT 26 | expect(store.getState().items).toEqual({ [todo.id]: todo }); 27 | }); 28 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | process.env.NODE_ENV = 'test'; 5 | 6 | const babelConfig = require('./.babelrc.js'); 7 | 8 | module.exports = (wallaby) => ({ 9 | files: [ 10 | 'tests/lib/*.js', 11 | 'tests/utils.js', 12 | 'src/**/*.js', 13 | { pattern: 'tests/**/*.test.js', ignore: true }, 14 | { pattern: 'tests/typescript/**/*', ignore: true }, 15 | ], 16 | tests: [ 17 | 'tests/*.test.js', 18 | 'tests/**/*.test.js', 19 | { pattern: 'tests/typescript.test.js', ignore: true }, 20 | ], 21 | testFramework: 'jest', 22 | env: { 23 | type: 'node', 24 | runner: 'node', 25 | }, 26 | compilers: { 27 | 'src/**/*.js': wallaby.compilers.babel(babelConfig), 28 | 'tests/**/*.js': wallaby.compilers.babel(babelConfig), 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // React 18 requires the use of the useSyncExternalStore hook for external 2 | // stores to hook into its concurrent features. We want to continue supporting 3 | // older versions of React (16/17), so we are utilsing a shim provided by the 4 | // React team which will ensure backwards compatibility; 5 | // eslint-disable-next-line import/no-unresolved 6 | import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'; 7 | 8 | import { initializeUseStoreState } from './hooks'; 9 | 10 | initializeUseStoreState(useSyncExternalStoreWithSelector); 11 | 12 | export * from './hooks'; 13 | export * from './create-store'; 14 | export * from './create-context-store'; 15 | export * from './create-transform'; 16 | export * from './provider'; 17 | export * from './use-local-store'; 18 | export * from './helpers'; 19 | -------------------------------------------------------------------------------- /tests/typescript/action.ts: -------------------------------------------------------------------------------- 1 | import { createStore, action, Action, Thunk, thunk } from 'easy-peasy'; 2 | 3 | interface TodosModel { 4 | items: string[]; 5 | add: Action; 6 | clear: Action; 7 | } 8 | 9 | interface Model { 10 | todos: TodosModel; 11 | } 12 | 13 | const todos: TodosModel = { 14 | items: [], 15 | add: action( 16 | (state, payload) => { 17 | state.items.push(payload); 18 | }, 19 | { immer: true }, 20 | ), 21 | clear: action((state) => { 22 | state.items = []; 23 | }), 24 | }; 25 | 26 | const model: Model = { 27 | todos, 28 | }; 29 | 30 | const store = createStore(model); 31 | 32 | store.dispatch.todos.add('foo'); 33 | // @ts-expect-error 34 | store.dispatch.todos.add(1); 35 | // @ts-expect-error 36 | store.dispatch.todos.add(); 37 | 38 | store.dispatch.todos.clear(); 39 | -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/release/java/com/reactnativetodo/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.reactnativetodo; 8 | 9 | import android.content.Context; 10 | import com.facebook.react.ReactInstanceManager; 11 | 12 | /** 13 | * Class responsible of loading Flipper inside your React Native application. This is the release 14 | * flavor of it so it's empty as we don't want to load Flipper. 15 | */ 16 | public class ReactNativeFlipper { 17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 18 | // Do nothing as we don't want to initialize Flipper on Release. 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /website/docs/docs/typescript-api/state.md: -------------------------------------------------------------------------------- 1 | # State 2 | 3 | Allows you to get a type that represents the state of a model. 4 | 5 | ```typescript 6 | State< 7 | Model extends object = {} 8 | > 9 | ``` 10 | 11 | ## Example 12 | 13 | ```typescript 14 | import { State } from 'easy-peasy'; 15 | import { StoreModel } from './index'; 16 | 17 | type StoreState = State; 18 | ``` 19 | 20 | Typically this would only be useful when using the `useStoreState` hook. 21 | 22 | ```typescript 23 | import { useStoreState, State } from 'easy-peasy'; 24 | import { StoreModel } from './store'; 25 | 26 | function MyComponent() { 27 | const todos = useStoreState( 28 | (state: State) => state.todos.items 29 | ); 30 | } 31 | ``` 32 | 33 | That being said, we recommend you use the [createTypedHooks](/docs/typescript-api/create-typed-hooks.html) API instead. -------------------------------------------------------------------------------- /tests/typescript/create-store.ts: -------------------------------------------------------------------------------- 1 | import { action, createStore, EasyPeasyConfig, Action } from 'easy-peasy'; 2 | 3 | interface StoreModel { 4 | foo: string; 5 | update: Action; 6 | } 7 | 8 | const model: StoreModel = { 9 | foo: 'bar', 10 | update: action((state, payload) => { 11 | state.foo = payload; 12 | }), 13 | }; 14 | 15 | const storeWithoutConfig = createStore(model); 16 | 17 | storeWithoutConfig.getMockedActions().length; 18 | storeWithoutConfig.clearMockedActions(); 19 | storeWithoutConfig.getState().foo; 20 | storeWithoutConfig.getActions().update('bar'); 21 | 22 | const config: EasyPeasyConfig = { 23 | mockActions: true, 24 | }; 25 | const storeWithConfig = createStore(model, config); 26 | 27 | storeWithConfig.getMockedActions().length; 28 | storeWithoutConfig.clearMockedActions(); 29 | storeWithConfig.getActions().update('bar'); 30 | -------------------------------------------------------------------------------- /src/migrations.js: -------------------------------------------------------------------------------- 1 | import { produce, setAutoFreeze } from 'immer'; 2 | 3 | export const migrate = ( 4 | data, 5 | migrations, 6 | ) => { 7 | setAutoFreeze(false); 8 | 9 | let version = data._migrationVersion ?? 0; 10 | const toVersion = migrations.migrationVersion 11 | 12 | if (typeof version !== "number" || typeof toVersion !== 'number') { 13 | throw new Error('No migration version found'); 14 | } 15 | 16 | while (version < toVersion) { 17 | const nextVersion = version + 1; 18 | const migrator = migrations[nextVersion]; 19 | 20 | if (!migrator) { 21 | throw new Error(`No migrator found for \`migrationVersion\` ${nextVersion}`); 22 | } 23 | 24 | data = produce(data, migrator); 25 | data._migrationVersion = nextVersion; 26 | version = data._migrationVersion; 27 | } 28 | 29 | setAutoFreeze(true); 30 | return data; 31 | } 32 | -------------------------------------------------------------------------------- /examples/react-native-todo/ios/ReactNativeTodoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/nextjs-todo/README.md: -------------------------------------------------------------------------------- 1 | # Simple todo example with Next.js 2 | 3 | This is a [Next.js](https://nextjs.org/) example bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 4 | 5 | This is a clone of [`simple-todo`](../simple-todo/), modified to be compatible with Next.js. 6 | 7 | ## Getting Started 8 | 9 | First, run the development server: 10 | 11 | ```bash 12 | yarn dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 18 | 19 | The `easy-peasy` store & models are located in the `store` folder. All pages (in this example, just `index.tsx`) 20 | are setup to use `easy-peasy` via the `pages/_app.tsx`, which wraps all page components with the ``. -------------------------------------------------------------------------------- /website/docs/docs/typescript-api/actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | Allows you to get a type that represents the actions of a model. 4 | 5 | ```typescript 6 | Actions< 7 | Model extends Object = {} 8 | > 9 | ``` 10 | 11 | ## Example 12 | 13 | ```typescript 14 | import { Actions } from 'easy-peasy'; 15 | import { StoreModel } from './index'; 16 | 17 | type StoreActions = Actions; 18 | ``` 19 | 20 | Typically this would only be useful when using the `useStoreActions` hook. 21 | 22 | ```typescript 23 | import { useStoreActions, Actions } from 'easy-peasy'; 24 | import { StoreModel } from './store'; 25 | 26 | function MyComponent() { 27 | const addTodo = useStoreActions( 28 | (actions: Actions) => actions.todos.addTodo 29 | ); 30 | } 31 | ``` 32 | 33 | That being said, we recommend you use the [createTypedHooks](/docs/typescript-api/create-typed-hooks.html) API instead. -------------------------------------------------------------------------------- /website/docs/docs/typescript-api/reducer.md: -------------------------------------------------------------------------------- 1 | # Reducer 2 | 3 | Defines a [reducer](/docs/api/reducer.html) against your model. 4 | 5 | ## API 6 | 7 | ```typescript 8 | Reducer< 9 | State = any, 10 | Action extends ReduxAction = ReduxAction 11 | > 12 | ``` 13 | 14 | - `State` 15 | 16 | The type for the state that will be managed by the [reducer](/docs/api/reducer.html). 17 | 18 | - `Action` 19 | 20 | The type of the actions that may be received by the reducer. 21 | 22 | 23 | ## Example 24 | 25 | ```typescript 26 | import { Reducer, reducer } from 'easy-peasy'; 27 | 28 | interface StoreModel { 29 | todos: Reducer; 30 | } 31 | 32 | const storeModel: StoreModel = { 33 | todos: reducer((state = [], action) => { 34 | switch (action.type) { 35 | case 'ADD_TODO': return [...state, action.payload]; 36 | default: return state; 37 | } 38 | }) 39 | } 40 | ``` -------------------------------------------------------------------------------- /src/create-transform.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file has been copied from redux-persist. 3 | * The intention being to support as much of the redux-persist API as possible. 4 | */ 5 | 6 | export function createTransform(inbound, outbound, config = {}) { 7 | const whitelist = config.whitelist || null; 8 | const blacklist = config.blacklist || null; 9 | 10 | function whitelistBlacklistCheck(key) { 11 | if (whitelist && whitelist.indexOf(key) === -1) return true; 12 | if (blacklist && blacklist.indexOf(key) !== -1) return true; 13 | return false; 14 | } 15 | 16 | return { 17 | in: (data, key, fullState) => 18 | !whitelistBlacklistCheck(key) && inbound 19 | ? inbound(data, key, fullState) 20 | : data, 21 | out: (data, key, fullState) => 22 | !whitelistBlacklistCheck(key) && outbound 23 | ? outbound(data, key, fullState) 24 | : data, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/pages/root.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Link, Navigate, Route, Routes } from 'react-router-dom'; 3 | import { StoreProvider } from 'easy-peasy'; 4 | import store from '@/store'; 5 | import PhotoGrid from '@/pages/photo-grid'; 6 | import Single from '@/pages/single'; 7 | 8 | export default function Root() { 9 | return ( 10 | <> 11 | 12 | 13 |

14 | Reduxstagram 15 |

16 | 17 | }> 18 | } /> 19 | } /> 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/simple-todo/README.md: -------------------------------------------------------------------------------- 1 | # Simple todo example ([View codesandbox](https://codesandbox.io/s/fnidh1)) 2 | 3 | The simplest example of `react` and `easy-peasy`. 4 | 5 | ![Todo app with easy-peasy](./resources/todo-app.gif) 6 | 7 | This is a `Vite + React + Typescript + Eslint + Prettier` example based on [this template](https://github.com/TheSwordBreaker/vite-reactts-eslint-prettier). 8 | 9 | ## Getting Started 10 | 11 | First, run the development server: 12 | 13 | ```bash 14 | yarn dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `src/components/App.tsx`. The page auto-updates as you edit the file. 20 | 21 | The `easy-peasy` store & models are located in the `src/store` folder. 22 | The `main.tsx` file wraps the `` component with the ``, so all child components can access the 23 | hooks exposed from the `store/index.ts`. 24 | -------------------------------------------------------------------------------- /examples/react-native-todo/README.md: -------------------------------------------------------------------------------- 1 | React Native Todo app based on React Native CLI quick start 2 | 3 | Store model copied (and modified) from [simple-todo](../simple-todo) 4 | 5 | ![React Native Todo with easy-peasy](./resources/todo.gif) 6 | 7 | ## Getting Started 8 | 9 | > Ensure that you have a working [development environment](https://reactnative.dev/docs/environment-setup?guide=native). 10 | 11 | First, install the dependencies: 12 | 13 | ```bash 14 | yarn 15 | ``` 16 | 17 | iOS 18 | 19 | ```bash 20 | yarn ios 21 | ``` 22 | 23 | Android 24 | 25 | ```bash 26 | yarn android 27 | ``` 28 | 29 | If metro does not start automatically, run: 30 | 31 | ```bash 32 | npx react-native start 33 | ``` 34 | 35 | You can start editing the screen by modifying `src/components/TodoList.tsx`. 36 | 37 | The `easy-peasy` store & models are located under `src/store`. 38 | The `App.tsx` file wraps the `` component with the ``. 39 | 40 | Happy coding! 🍏 41 | -------------------------------------------------------------------------------- /examples/reduxtagram/test/posts-model.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { createStore } from 'easy-peasy'; 3 | import { Post, postsModel } from '@/model'; 4 | 5 | describe('Posts', () => { 6 | it('should be initialized', () => { 7 | // arrange 8 | const store = createStore(postsModel, { initialState: { posts: [] } }); 9 | 10 | // assert 11 | expect(store.getState().posts).toEqual([]); 12 | }); 13 | 14 | it('should like post', () => { 15 | // arrange 16 | const post: Post = { 17 | caption: 'Lunch #hamont', 18 | likes: 8, 19 | id: 'BAcyDyQwcXX', 20 | src: 'https://picsum.photos/400/400/?image=64', 21 | }; 22 | const store = createStore(postsModel, { 23 | initialState: { posts: [post] }, 24 | }); 25 | 26 | // act 27 | store.getActions().likePost('BAcyDyQwcXX'); 28 | 29 | // assert 30 | expect(store.getState().posts).toEqual([{ ...post, likes: 9 }]); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /website/docs/docs/typescript-api/create-typed-hooks.md: -------------------------------------------------------------------------------- 1 | # createTypedHooks 2 | 3 | Creates typed versions of the hooks so that you don't need to apply typing information against them when using them within your components. 4 | 5 | ## Example 6 | 7 | ```typescript 8 | // hooks.js 9 | import { createTypedHooks } from 'easy-peasy'; 10 | import { StoreModel } from './model'; 11 | 12 | const { useStoreActions, useStoreState, useStoreDispatch, useStore } = createTypedHooks(); 13 | 14 | export { 15 | useStoreActions, 16 | useStoreState, 17 | useStoreDispatch, 18 | useStore 19 | } 20 | ``` 21 | 22 | And then use them within your components: 23 | 24 | ```typescript 25 | import { useStoreState } from './hooks'; // 👈 import the typed hooks 26 | 27 | export default MyComponent() { 28 | // This will be typed 29 | // 👇 30 | const message = useStoreState(state => state.message); 31 | return
{message}
; 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /examples/kanban/src/components/TaskList/TaskList.tsx: -------------------------------------------------------------------------------- 1 | import { useStoreState } from '../../store'; 2 | import { StoreModel } from '../../store/model'; 3 | import AddTask from './AddTask'; 4 | import TaskView from './TaskView'; 5 | 6 | const TaskList: React.FC<{ list: keyof StoreModel }> = ({ list }) => { 7 | const state = useStoreState((state) => state[list]); 8 | 9 | return ( 10 |
11 |

12 | {state.name} 13 |

14 |
15 |
    16 | {state.tasks.map((task) => ( 17 | 18 | ))} 19 |
20 |
21 | 22 |
23 |
24 |
25 | ); 26 | }; 27 | 28 | export default TaskList; 29 | -------------------------------------------------------------------------------- /website/docs/docs/introduction/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | Easy Peasy is a full abstraction over Redux, providing an API that is both intuitive and quick to develop against, whilst removing any need for boilerplate. By wrapping Redux we get to leverage its mature architecture, whilst also being able to support the amazing tooling that has formed around it. For example, we support the [Redux Dev Tools Extension](https://github.com/zalmoxisus/redux-devtools-extension) out of the box. 4 | 5 | In addition to this, as we are outputing a Redux store, this allows interop with existing libraries and applications that are using React Redux. A big benefit of this is that you can apply a gradual migration of your existing applications from React Redux into Easy Peasy. 6 | 7 | To help support migration and interoperability we expose configuration allowing the extension of the underlying Redux store via middleware and enhancers. 8 | 9 | That being said, absolutely no Redux experience is required to use Easy Peasy. 10 | -------------------------------------------------------------------------------- /tests/debug.test.js: -------------------------------------------------------------------------------- 1 | import { debug, action, createStore } from '../src'; 2 | import { mockConsole } from './utils'; 3 | 4 | let restore; 5 | 6 | beforeEach(() => { 7 | restore = mockConsole(); 8 | }); 9 | 10 | afterEach(() => { 11 | restore(); 12 | }); 13 | 14 | it('should return state with changes applied', () => { 15 | // ARRANGE 16 | const store = createStore({ 17 | logs: ['foo'], 18 | add: action((state, payload) => { 19 | expect(debug(state)).toEqual({ logs: ['foo'] }); 20 | state.logs.push(payload); 21 | expect(debug(state)).toEqual({ logs: ['foo', 'bar'] }); 22 | }), 23 | }); 24 | 25 | // ACT 26 | store.getActions().add('bar'); 27 | 28 | // ASSERT 29 | expect(store.getState()).toEqual({ logs: ['foo', 'bar'] }); 30 | }); 31 | 32 | it('returns argument when not a draft', () => { 33 | // ARRANGE 34 | const notADraft = { foo: 'bar' }; 35 | 36 | // ACT 37 | const actual = debug(notADraft); 38 | 39 | // ASSERT 40 | expect(actual).toBe(notADraft); 41 | }); 42 | -------------------------------------------------------------------------------- /website/docs/docs/typescript-api/action.md: -------------------------------------------------------------------------------- 1 | # Action 2 | 3 | Defines an [action](/docs/api/action.html) against your model 4 | 5 | ## API 6 | 7 | ```typescript 8 | Action< 9 | Model extends object = {}, 10 | Payload = void 11 | > 12 | ``` 13 | 14 | - `Model` 15 | 16 | The model against which the action is being defined. You need to provide this so that the state that will be provided to your [action](/docs/api/action.html) is correctly typed. 17 | 18 | - `Payload` 19 | 20 | The type of the payload that the [action](/docs/api/action.html) will receive. You can omit this if you do not expect the [action](/docs/api/action.html) to receive any payload. 21 | 22 | 23 | ## Example 24 | 25 | ```typescript 26 | import { Action, action } from 'easy-peasy'; 27 | 28 | interface TodosModel { 29 | todos: string[]; 30 | addTodo: Action; 31 | } 32 | 33 | const todosModel: TodosModel = { 34 | todos: [], 35 | addTodo: action((state, payload) => { 36 | state.todos.push(payload); 37 | }) 38 | } 39 | ``` -------------------------------------------------------------------------------- /website/docs/docs/recipes/react-native-devtools.md: -------------------------------------------------------------------------------- 1 | # React Native Dev Tools 2 | 3 | React Native, hybrid, desktop and server side Redux apps can use Redux Dev Tools using the [Remote Redux DevTools](https://github.com/zalmoxisus/remote-redux-devtools) library. 4 | 5 | To use this library, you will need to pass the DevTools compose helper as part of the [config object](/docs/api/store-config.html) to `createStore` 6 | 7 | ```javascript 8 | import { createStore } from 'easy-peasy'; 9 | import { composeWithDevTools } from 'remote-redux-devtools'; 10 | import model from './model'; 11 | 12 | const store = createStore(model, { 13 | compose: composeWithDevTools({ realtime: true, trace: true }) 14 | }); 15 | 16 | export default store; 17 | ``` 18 | 19 | In the example above you will see that we are extending our store, providing an override to the default `compose` function used for our Redux store enhancers. We are utilising the compose exported by the [remote-redux-devtools](https://github.com/zalmoxisus/remote-redux-devtools) library. 20 | -------------------------------------------------------------------------------- /website/docs/docs/introduction/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Easy Peasy is an abstraction of Redux. It provides a reimagined API focussing on 4 | developer experience, allowing you to quickly 5 | and easily manage your state, whilst leveraging the strong 6 | architectural guarantees and providing integration with the 7 | extensive eco-system that Redux has to offer. 8 | 9 | Batteries are included - no configuration is required to 10 | support a robust and scalable state management 11 | solution that includes advanced features such as derived state, API calls, 12 | developer tools, and fully typed experience via 13 | TypeScript. 14 | 15 | 18 | 19 |

20 | Quick Start 21 |

22 | -------------------------------------------------------------------------------- /examples/kanban/src/components/TaskList/AddTask.test.tsx: -------------------------------------------------------------------------------- 1 | import { createStore } from 'easy-peasy'; 2 | import { describe, it, expect } from 'vitest'; 3 | 4 | import model, { StoreModel } from '../../store/model'; 5 | import { setup } from '../../utils/test-utils'; 6 | import AddTask from './AddTask'; 7 | 8 | const listKeys: Array = ['todo', 'doing', 'done']; 9 | 10 | describe.each(listKeys)('', (list) => { 11 | it('should add tasks correctly', async () => { 12 | const store = createStore(model); 13 | const { user, getByRole } = setup(, { store }); 14 | 15 | await user.type( 16 | getByRole('textbox', { name: new RegExp(`task name for "${list}"`, 'i') }), 17 | 'My new task', 18 | ); 19 | await user.click( 20 | getByRole('button', { name: new RegExp(`add task for "${list}"`, 'i') }), 21 | ); 22 | 23 | const taskList = store.getState()[list].tasks; 24 | expect(taskList).toContainEqual({ id: expect.any(String), name: 'My new task' }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/styles/_typography.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 10px; 3 | font-family: sans-serif; 4 | } 5 | 6 | p { 7 | font-size: 1.6rem; 8 | line-height: 1.5; 9 | } 10 | 11 | h1 { 12 | font-family: billabong, 'billabongregular', serif; 13 | text-align: center; 14 | font-weight: 100; 15 | font-size: 13rem; 16 | margin: 2rem 0; 17 | letter-spacing: -1px; 18 | text-shadow: 0 4px 0 rgba(18, 86, 136, 0.11); 19 | } 20 | 21 | h1 a { 22 | color: var(--blue); 23 | text-decoration: none; 24 | } 25 | 26 | h1 a:focus { 27 | outline: 0; 28 | } 29 | 30 | @font-face { 31 | font-family: 'billabongregular'; 32 | src: url('../assets/fonts/billabong-webfont.eot'); 33 | src: url('../assets/fonts/billabong-webfont.eot?#iefix') format('embedded-opentype'), 34 | url('../assets/fonts/billabong-webfont.woff') format('woff'), 35 | url('../assets/fonts/billabong-webfont.ttf') format('truetype'), 36 | url('../assets/fonts/billabong-webfont.svg#billabongregular') format('svg'); 37 | font-weight: normal; 38 | font-style: normal; 39 | } 40 | -------------------------------------------------------------------------------- /tests/typescript/complex-store-inheritance.ts: -------------------------------------------------------------------------------- 1 | import { Thunk } from 'easy-peasy'; 2 | 3 | type Injections = any; 4 | 5 | type ISomeOtherModel = {}; 6 | 7 | interface IModelA { 8 | commitSomething: Thunk< 9 | IModelAStoreModel, 10 | void, 11 | Injections, 12 | IModelAStoreModel 13 | >; 14 | } 15 | 16 | // IModelAStoreModel is generic, and requires the root store as input 17 | type IModelAStoreModel = IModelA & 18 | ISomeOtherModel & { 19 | onSomethingLoaded: Thunk; 20 | }; 21 | 22 | // IModelBStoreModel is not generic, and is based on IModelAStoreModel, but it does not care about the generic type 23 | // of IModelAStoreModel (thus it is just passing`any` to`IModelAStoreModel`) 24 | interface IModelBStoreModel extends IModelAStoreModel {} 25 | 26 | // IStoreModel should be allowed to extend both IModelAStoreModel & IModelBStoreModel 27 | interface IStoreModel 28 | extends IModelAStoreModel, 29 | IModelBStoreModel {} 30 | -------------------------------------------------------------------------------- /examples/kanban/src/components/TaskList/TaskList.test.tsx: -------------------------------------------------------------------------------- 1 | import { createStore } from 'easy-peasy'; 2 | import { describe, it, expect } from 'vitest'; 3 | 4 | import model, { StoreModel } from '../../store/model'; 5 | import { setup } from '../../utils/test-utils'; 6 | import TaskList from './TaskList'; 7 | 8 | const listKeys: Array = ['todo', 'doing', 'done']; 9 | 10 | describe.each(listKeys)('', (list) => { 11 | it('should render the tasks correctly', () => { 12 | const store = createPopulatedStore(); 13 | const { container } = setup(, { store }); 14 | 15 | expect(container).toMatchSnapshot(); 16 | }); 17 | }); 18 | 19 | const createPopulatedStore = () => { 20 | const store = createStore(model); 21 | for (let i = 0; i < 3; i++) { 22 | store.getActions().todo.addTask({ id: `todo-${i}`, name: `Todo ${i}` }); 23 | store.getActions().doing.addTask({ id: `doing-${i}`, name: `Doing ${i}` }); 24 | store.getActions().done.addTask({ id: `done-${i}`, name: `Done ${i}` }); 25 | } 26 | return store; 27 | }; 28 | -------------------------------------------------------------------------------- /website/docs/docs/community-extensions/README.md: -------------------------------------------------------------------------------- 1 | # Community Extensions 2 | 3 | Below is a list of some of the work performed by the community, providing some interesting extensions to Easy Peasy. 4 | 5 | - [`easy-peasy-decorators`](#easy-peasy-decoratorshttpsgithubcomeasypeasy-communitydecorators) 6 | 7 | ## [`easy-peasy-decorators`](https://github.com/easypeasy-community/decorators) 8 | 9 | This is a lightweight TypeScript library, providing the ability to generate stores via classes and decorators. 10 | 11 | ```typescript 12 | import { Model, Property, Action, createStore } from 'easy-peasy-decorators'; 13 | 14 | @Model('todos') 15 | class TodoModel { 16 | @Property() 17 | public items = ['Create store', 'Wrap application', 'Use store']; 18 | 19 | @Action() 20 | add(payload: string) { 21 | this.items.push(payload); 22 | } 23 | } 24 | 25 | interface IStoreModel { 26 | todos: TodoModel; 27 | } 28 | 29 | export const store = createStore(); 30 | ``` 31 | 32 | Check out the [GitHub repository](https://github.com/easypeasy-community/decorators) for more information. 33 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/pages/single.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useParams } from 'react-router'; 3 | import AddCommentForm from '@/components/add-comment-form'; 4 | import Photo from '@/components/photo'; 5 | import PostComment from '@/components/post-comment'; 6 | import { useStoreState } from '@/hooks'; 7 | 8 | export default function Single() { 9 | const { postId } = useParams<{ postId: string }>(); 10 | const post = useStoreState((state) => state.postsModel.posts.find((item) => item.id === postId)); 11 | const postComments = useStoreState((state) => (post ? state.commentsModel.byPostId(post.id) : [])); 12 | 13 | if (post) { 14 | return ( 15 |
16 | 17 |
18 | {postComments.map((comment) => ( 19 | 20 | ))} 21 | 22 |
23 |
24 | ); 25 | } else { 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /website/docs/docs/known-issues/using-keyof-in-generic-typescript-model.md: -------------------------------------------------------------------------------- 1 | # Using `keyof T` within a generic TypeScript model 2 | 3 | When defining generic model helpers via TypeScript you will be unable to put a restriction within your generic model based on the `keyof` the incoming generic model argument. This is illustrated below. 4 | 5 | ```typescript 6 | import { computed, Computed, action, Action, thunk, Thunk } from "easy-peasy"; 7 | 8 | export interface DataModel { 9 | items: Array; 10 | count: Computed, number>; 11 | // This is not supported. It currently breaks the Easy Peasy typings, 12 | // resulting in the `dataModel` helper below not presenting the correct type 13 | // information to you. 14 | // 👇 15 | sortBy: keyof Item | "none"; 16 | } 17 | 18 | export const dataModel = (items: Item[]): DataModel => ({ 19 | items: items, 20 | // This typing information would be invalid 21 | // 👇 22 | count: computed(state => state.items.length), 23 | sortBy: "none" 24 | }); 25 | ``` -------------------------------------------------------------------------------- /examples/nextjs-todo/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /examples/react-native-todo/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /website/docs/docs/api/use-store-actions.md: -------------------------------------------------------------------------------- 1 | # useStoreActions 2 | 3 | A [hook](https://reactjs.org/docs/hooks-intro.html) granting your components access to the [store's](/docs/api/store.html) [actions](/docs/api/action.html). 4 | 5 | ```javascript 6 | const addTodo = useStoreActions(actions => actions.todos.add); 7 | ``` 8 | 9 | ## Arguments 10 | 11 | - `mapActions` (Function, required) 12 | 13 | The function that is used to resolve the [action](/docs/api/action.html) that your component requires. It receives the following arguments: 14 | 15 | - `actions` (Object) 16 | 17 | The [actions](/docs/api/action.html) of your store. 18 | 19 | ## Example 20 | 21 | ```javascript 22 | import { useState } from 'react'; 23 | import { useStoreActions } from 'easy-peasy'; 24 | 25 | const AddTodo = () => { 26 | const [text, setText] = useState(''); 27 | const addTodo = useStoreActions(actions => actions.todos.add); 28 | return ( 29 |
30 | setText(e.target.value)} /> 31 | 32 |
33 | ); 34 | }; 35 | ``` 36 | -------------------------------------------------------------------------------- /examples/react-native-todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactNativeTodo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "lint": "eslint .", 9 | "start": "react-native start", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "easy-peasy": "^6.0.0", 14 | "react": "18.2.0", 15 | "react-native": "0.71.4" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.20.0", 19 | "@babel/preset-env": "^7.20.0", 20 | "@babel/runtime": "^7.20.0", 21 | "@react-native-community/eslint-config": "^3.2.0", 22 | "@tsconfig/react-native": "^2.0.2", 23 | "@types/jest": "^29.2.1", 24 | "@types/react": "^18.0.24", 25 | "@types/react-test-renderer": "^18.0.0", 26 | "babel-jest": "^29.2.1", 27 | "eslint": "^8.19.0", 28 | "jest": "^29.2.1", 29 | "metro-react-native-babel-preset": "0.73.8", 30 | "prettier": "^2.4.1", 31 | "react-test-renderer": "18.2.0", 32 | "typescript": "4.8.4" 33 | }, 34 | "jest": { 35 | "preset": "react-native" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /website/docs/docs/known-issues/typescript-optional-computed-properties.md: -------------------------------------------------------------------------------- 1 | # Marking computed properties as optional on your TypeScript model 2 | 3 | Unfortunately, due to the way our typing system maps your model, you cannot 4 | declare a computed property as being optional via the `?` property postfix. 5 | 6 | For example: 7 | 8 | ```typescript 9 | interface StoreModel { 10 | products: Product[]; 11 | totalPrice?: Computed; 12 | // 👆 13 | // Note the optional definition 14 | } 15 | 16 | const storeModel: StoreModel = { 17 | products: []; 18 | // This will result in a TypeScript error 😢 19 | totalPrice: computed( 20 | state => state.products.length > 0 21 | ? calcPrice(state.products) 22 | : undefined 23 | ) 24 | } 25 | ``` 26 | 27 | Luckily there is a workaround; simply adjust the definition of your computed 28 | property to indicate that the result could be undefined. 29 | 30 | ```diff 31 | interface StoreModel { 32 | products: Product[]; 33 | - totalPrice?: Computed; 34 | + totalPrice: Computed; 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present Sean Matheson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/reduxtagram/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "overrides": [ 9 | { 10 | "files": [ 11 | "*.js" 12 | ], 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:prettier/recommended" 16 | ] 17 | }, 18 | { 19 | "files": [ 20 | "*.ts", 21 | "*.tsx" 22 | ], 23 | "settings": { 24 | "react": { 25 | "version": "detect" 26 | } 27 | }, 28 | "parser": "@typescript-eslint/parser", 29 | "parserOptions": { 30 | "ecmaFeatures": { 31 | "jsx": true 32 | }, 33 | "ecmaVersion": "latest", 34 | "sourceType": "module" 35 | }, 36 | "extends": [ 37 | "eslint:recommended", 38 | "plugin:@typescript-eslint/recommended", 39 | "plugin:react/recommended", 40 | "plugin:prettier/recommended" 41 | ], 42 | "rules": { 43 | "react/react-in-jsx-scope": "off", 44 | "@typescript-eslint/no-non-null-assertion": "off" 45 | } 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /examples/react-native-todo/ios/ReactNativeTodo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/typescript/issue427.ts: -------------------------------------------------------------------------------- 1 | import { Computed, computed } from 'easy-peasy'; 2 | 3 | interface AppModel { 4 | currentRoleId?: number; 5 | } 6 | 7 | interface Role { 8 | id: number; 9 | name: string; 10 | description: string; 11 | } 12 | 13 | interface RolesModel { 14 | rolesMap: { [roleId: string]: Role }; 15 | currentRole: Computed; 16 | currentRoleName: Computed; 17 | } 18 | 19 | interface StoreModel { 20 | app: AppModel; 21 | roles: RolesModel; 22 | } 23 | 24 | const storeModel: StoreModel = { 25 | app: { 26 | currentRoleId: 1, 27 | }, 28 | roles: { 29 | rolesMap: { 30 | '1': { id: 1, name: 'Role example', description: 'Role description' }, 31 | }, 32 | currentRole: computed( 33 | [ 34 | (_, storeState) => storeState.app.currentRoleId, 35 | (state) => state.rolesMap, 36 | ], 37 | (roleId, rolesMap) => (roleId != null ? rolesMap[roleId] : undefined), 38 | ), 39 | currentRoleName: computed( 40 | [(state) => state.currentRole], 41 | (role) => role && role.name, 42 | ), 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /tests/typescript/use-local-store.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStore, Action, action } from 'easy-peasy'; 2 | 3 | interface StoreModel { 4 | count: number; 5 | inc: Action; 6 | } 7 | 8 | const [state, actions, store] = useLocalStore(() => ({ 9 | count: 0, 10 | inc: action((state) => { 11 | state.count += 1; 12 | }), 13 | })); 14 | 15 | state.count + 1; 16 | actions.inc(); 17 | store.getState().count + 1; 18 | 19 | useLocalStore( 20 | () => ({ 21 | count: 0, 22 | inc: action((state) => { 23 | state.count += 1; 24 | }), 25 | }), 26 | ['foo', 123], 27 | ); 28 | 29 | useLocalStore( 30 | (prevState) => { 31 | if (prevState != null) { 32 | prevState.count + 1; 33 | } 34 | return { 35 | count: 0, 36 | inc: action((state) => { 37 | state.count += 1; 38 | }), 39 | }; 40 | }, 41 | ['foo', 123], 42 | (prevState, prevConfig) => { 43 | if (prevState != null) { 44 | prevState.count + 1; 45 | } 46 | if (prevConfig != null) { 47 | `${prevConfig.name}foo`; 48 | } 49 | return { 50 | name: 'MyLocalStore', 51 | }; 52 | }, 53 | ); 54 | -------------------------------------------------------------------------------- /examples/reduxtagram/src/components/add-comment-form.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, useState } from 'react'; 2 | import { useStoreActions } from '@/hooks'; 3 | 4 | interface Props { 5 | postId: string; 6 | } 7 | 8 | export default function AddCommentForm({ postId }: Props) { 9 | const addComment = useStoreActions((actions) => actions.commentsModel.addComment); 10 | const [author, setAuthor] = useState(''); 11 | const [comment, setComment] = useState(''); 12 | 13 | const onSubmit = (e: FormEvent) => { 14 | e.preventDefault(); 15 | 16 | if (author && comment) { 17 | addComment({ postId, user: author, text: comment }); 18 | setAuthor(''); 19 | setComment(''); 20 | } 21 | }; 22 | 23 | return ( 24 |
25 | setAuthor(e.target.value)} placeholder="author" required /> 26 | setComment(e.target.value)} placeholder="comment" required /> 27 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /website/docs/docs/api/debug.md: -------------------------------------------------------------------------------- 1 | # debug 2 | 3 | This helper is useful in the context of [actions](/docs/api/action.html). 4 | 5 | [Actions](/docs/api/action.html) use the [immer](https://github.com/mweststrate/immer) library under the hood in order to convert mutative updates into immutable ones. Therefore if you try to `console.log` your state within an [action](/doc/api/action.html) you will see a `Proxy` object or a `null` is printed. 6 | 7 | Use this helper in order to get the actual value of the `state` within your [action](/docs/api/action.html). 8 | 9 | _Before:_ 10 | 11 | ```javascript 12 | const model = { 13 | increment: action((state, payload) => { 14 | state.count += 1; 15 | console.log(state); // 👈 prints a Proxy object or a null 16 | }) 17 | } 18 | ``` 19 | 20 | _After:_ 21 | 22 | ```javascript 23 | import { debug } from 'easy-peasy'; 24 | 25 | const model = { 26 | increment: action((state, payload) => { 27 | state.count += 1; 28 | console.log(debug(state)); // 👈 prints the "native" state representation 29 | }) 30 | } 31 | ``` 32 | 33 | > ***Note:*** *If you have set the `disableImmer` configuration value on the store you will not need to use this helper.* -------------------------------------------------------------------------------- /website/docs/docs/recipes/connecting-to-reactotron.md: -------------------------------------------------------------------------------- 1 | # Connecting to Reactotron 2 | 3 | [Reactotron](https://github.com/infinitered/reactotron) is a desktop app for 4 | inspecting your React JS and React Native projects. 5 | 6 | It is possible to configure Easy Peasy so to be connected to your Reactotron 7 | instance. 8 | 9 | Firstly, ensure you have a Reactotron configuration similar to. 10 | 11 | ```javascript 12 | // reactotron.js 13 | 14 | import Reactotron from 'reactotron-react-native'; 15 | import { reactotronRedux } from 'reactotron-redux'; 16 | 17 | const reactron = Reactotron.configure() 18 | .useReactNative() 19 | .use(reactotronRedux()) 20 | .connect(); 21 | 22 | export default reactron; 23 | ``` 24 | 25 | Then update the manner in which you create your Easy Peasy store. 26 | 27 | ```javascript 28 | // create-store.js 29 | 30 | import { createStore } from 'easy-peasy'; 31 | import model from './model'; 32 | 33 | let storeEnhancers = []; 34 | 35 | if (__DEV__) { 36 | const reactotron = require('./reactotron').default; 37 | storeEnhancers = [...storeEnhancers, reactotron.createEnhancer()]; 38 | } 39 | 40 | const store = createStore(model, { 41 | enhancers: [...storeEnhancers], 42 | }); 43 | 44 | export default store; 45 | ``` 46 | -------------------------------------------------------------------------------- /website/docs/docs/typescript-api/action-on.md: -------------------------------------------------------------------------------- 1 | # ActionOn 2 | 3 | Defines an [actionOn](/docs/api/action-on.html) listener against your model. 4 | 5 | ## API 6 | 7 | ```typescript 8 | ActionOn< 9 | Model extends object = {}, 10 | StoreModel extends object = {} 11 | > 12 | ``` 13 | 14 | - `Model` 15 | 16 | The model against which the [actionOn](/docs/api/action-on.html) is being defined. You need to provide this so that the state that will be provided to your [actionOn](/docs/api/action-on.html) is correctly typed. 17 | 18 | - `StoreModel` 19 | 20 | If you plan on targeting an action from another part of your store state then you will need to provide your store model so that the provided store actions are correctly typed. 21 | 22 | 23 | ## Example 24 | 25 | ```typescript 26 | import { ActionOn, actionOn } from 'easy-peasy'; 27 | import { StoreModel } from '../index'; 28 | 29 | interface AuditModel { 30 | logs: string[]; 31 | onTodoAdded: ActionOn; 32 | } 33 | 34 | const auditModel: AuditModel = { 35 | logs: [], 36 | onTodoAdded: actionOn( 37 | (actions, storeActions) => storeActions.todos.addTodo, 38 | (state, payload) => { 39 | state.logs.push(`Added todo: ${payload}`); 40 | } 41 | ) 42 | } 43 | ``` -------------------------------------------------------------------------------- /tests/typescript/issue246.ts: -------------------------------------------------------------------------------- 1 | import { Action, action, createStore } from 'easy-peasy'; 2 | 3 | interface Item { 4 | id: number; 5 | name: string; 6 | } 7 | 8 | interface BeholderModel extends BaseListModel { 9 | // Extra stuff for interface 10 | beholderName: string; 11 | } 12 | 13 | interface CreatorModel extends BaseListModel { 14 | // Extra stuff for interface 15 | creatorName: string; 16 | } 17 | 18 | // error at in setList 19 | interface BaseListModel { 20 | list: Item[]; 21 | setList: Action; 22 | } 23 | 24 | interface StoreModel { 25 | beholder: BeholderModel; 26 | creator: CreatorModel; 27 | } 28 | 29 | const model: StoreModel = { 30 | beholder: { 31 | beholderName: 'foo', 32 | list: [], 33 | setList: action((state, payload) => { 34 | state.list = payload; 35 | }), 36 | }, 37 | creator: { 38 | creatorName: 'foo', 39 | list: [], 40 | setList: action((state, payload) => { 41 | state.list = payload; 42 | }), 43 | }, 44 | }; 45 | 46 | const store = createStore(model); 47 | 48 | store.getState().beholder.beholderName; 49 | store.getState().beholder.list; 50 | store.getActions().creator.setList([{ id: 1, name: 'foo' }]); 51 | -------------------------------------------------------------------------------- /tests/immer.test.js: -------------------------------------------------------------------------------- 1 | import './lib/enable-immer-map-set'; 2 | import React, { act } from 'react'; 3 | import { render } from '@testing-library/react'; 4 | import { createStore, action, StoreProvider, useStoreState } from '../src'; 5 | 6 | test('Map and Set within a store work as expected', () => { 7 | // ARRANGE 8 | const store = createStore({ 9 | products: new Set(), 10 | addProduct: action((state, payload) => { 11 | state.products.add(payload); 12 | }), 13 | }); 14 | 15 | function App() { 16 | const products = useStoreState((state) => state.products); 17 | const productsArray = [...products]; 18 | return ( 19 |
20 | {productsArray.length === 0 ? 'none' : productsArray.join(',')} 21 |
22 | ); 23 | } 24 | 25 | const { getByTestId } = render( 26 | 27 | 28 | , 29 | ); 30 | 31 | // ASSERT 32 | expect(getByTestId('products').textContent).toBe('none'); 33 | 34 | // ACT 35 | act(() => { 36 | store.getActions().addProduct('potato'); 37 | store.getActions().addProduct('avocado'); 38 | }); 39 | 40 | // ASSERT 41 | expect(getByTestId('products').textContent).toBe('potato,avocado'); 42 | }); 43 | -------------------------------------------------------------------------------- /examples/react-native-todo/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | ios/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | *.keystore 35 | !debug.keystore 36 | 37 | # node.js 38 | # 39 | node_modules/ 40 | npm-debug.log 41 | yarn-error.log 42 | 43 | # fastlane 44 | # 45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 46 | # screenshots whenever they are needed. 47 | # For more information about the recommended setup visit: 48 | # https://docs.fastlane.tools/best-practices/source-control/ 49 | 50 | **/fastlane/report.xml 51 | **/fastlane/Preview.html 52 | **/fastlane/screenshots 53 | **/fastlane/test_output 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # Ruby / CocoaPods 59 | /ios/Pods/ 60 | /vendor/bundle/ 61 | 62 | # Temporary files created by Metro to check the health of the file watcher 63 | .metro-health-check* 64 | -------------------------------------------------------------------------------- /website/docs/docs/recipes/hot-reloading.md: -------------------------------------------------------------------------------- 1 | # Hot Reloading 2 | 3 | Easy Peasy supports hot reloading - i.e. being able to dynamically update your model at development time whilst maintaining the current state of your application. This can lead to a much improved developer experience. 4 | 5 | In order to configure your application to allow hot reloading of your Easy Peasy store you will need to do the following: 6 | 7 | ```javascript 8 | // src/store/index.js 9 | 10 | import { createStore } from "easy-peasy"; 11 | import model from "./model"; 12 | 13 | const store = createStore(model); 14 | 15 | // Wrapping dev only code like this normally gets stripped out by bundlers 16 | // such as Webpack when creating a production build. 17 | if (process.env.NODE_ENV === "development") { 18 | if (module.hot) { 19 | module.hot.accept("./model", () => { 20 | store.reconfigure(model); // 👈 Here is the magic 21 | }); 22 | } 23 | } 24 | 25 | export default store; 26 | ``` 27 | 28 | Note how you can call the [store's](/docs/api/store.html) `reconfigure` method in order to reconfigure the store with your updated model. The existing state will be maintained. 29 | 30 | You can [view a demo repository configured for hot reloading here](https://github.com/ctrlplusb/easy-peasy-hot-reload). -------------------------------------------------------------------------------- /src/use-local-store.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { useMemoOne } from './lib'; 3 | import { createStore } from './create-store'; 4 | 5 | export function useLocalStore( 6 | modelCreator, 7 | dependencies = [], 8 | configCreator = null, 9 | ) { 10 | const storeRef = useRef(); 11 | 12 | const configRef = useRef(); 13 | 14 | const store = useMemoOne(() => { 15 | const previousState = 16 | storeRef.current != null ? storeRef.current.getState() : undefined; 17 | const config = 18 | configCreator != null 19 | ? configCreator(previousState, configRef.current) 20 | : undefined; 21 | const _store = createStore(modelCreator(previousState), config); 22 | configRef.current = config; 23 | storeRef.current = _store; 24 | return _store; 25 | }, dependencies); 26 | 27 | const [currentState, setCurrentState] = useState(() => store.getState()); 28 | 29 | useEffect(() => { 30 | setCurrentState(store.getState()); 31 | return store.subscribe(() => { 32 | const nextState = store.getState(); 33 | if (currentState !== nextState) { 34 | setCurrentState(nextState); 35 | } 36 | }); 37 | }, [store]); 38 | 39 | return [currentState, store.getActions(), store]; 40 | } 41 | -------------------------------------------------------------------------------- /tests/testing/listeners.test.js: -------------------------------------------------------------------------------- 1 | import { action, createStore, actionOn } from '../../src'; 2 | 3 | const model = { 4 | todos: [], 5 | logs: [], 6 | addTodo: action((state, payload) => { 7 | state.todos.push(payload); 8 | }), 9 | onTodoAdded: actionOn( 10 | (actions) => actions.addTodo, 11 | (state, target) => { 12 | state.logs.push(`Added todo: ${target.payload}`); 13 | }, 14 | ), 15 | }; 16 | 17 | test('listener gets dispatched when target fires', () => { 18 | // ARRANGE 19 | const store = createStore(model, { 20 | mockActions: true, 21 | }); 22 | 23 | // ACT 24 | store.getActions().addTodo('Write docs'); 25 | 26 | // ASSERT 27 | expect(store.getMockedActions()).toMatchObject([ 28 | { type: '@action.addTodo', payload: 'Write docs' }, 29 | { 30 | type: '@actionOn.onTodoAdded', 31 | payload: { 32 | type: '@action.addTodo', 33 | payload: 'Write docs', 34 | }, 35 | }, 36 | ]); 37 | }); 38 | 39 | test('listener acts as expected', () => { 40 | // ARRANGE 41 | const store = createStore(model); 42 | 43 | // ACT 44 | store.getListeners().onTodoAdded({ 45 | type: '@action.addTodo', 46 | payload: 'Test listeners', 47 | }); 48 | 49 | // ASSERT 50 | expect(store.getState().logs).toEqual(['Added todo: Test listeners']); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/react-native-todo/ios/ReactNativeTodo/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | self.moduleName = @"ReactNativeTodo"; 10 | // You can add your custom initial props in the dictionary below. 11 | // They will be passed down to the ViewController used by React Native. 12 | self.initialProps = @{}; 13 | 14 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 15 | } 16 | 17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 18 | { 19 | #if DEBUG 20 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 21 | #else 22 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 23 | #endif 24 | } 25 | 26 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. 27 | /// 28 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html 29 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). 30 | /// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`. 31 | - (BOOL)concurrentRootEnabled 32 | { 33 | return true; 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /examples/react-native-todo/src/components/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import React, {PropsWithChildren} from 'react'; 2 | import {View, Switch, Text, StyleSheet} from 'react-native'; 3 | import {useStoreState} from '../store'; 4 | 5 | type Props = PropsWithChildren<{ 6 | isEnabled: boolean; 7 | setIsEnabled: Function; 8 | }>; 9 | 10 | const Toolbar = ({isEnabled, setIsEnabled}: Props): JSX.Element => { 11 | const {completedCount, totalCount} = useStoreState(state => state); 12 | 13 | const toggleSwitch = () => { 14 | setIsEnabled((previousState: Boolean) => !previousState); 15 | }; 16 | 17 | return ( 18 | 19 | 20 | {completedCount} of {totalCount} 21 | 22 | 23 | Hide Done 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | const styles = StyleSheet.create({ 31 | row: { 32 | flexDirection: 'row', 33 | justifyContent: 'space-between', 34 | alignItems: 'center', 35 | paddingVertical: 10, 36 | }, 37 | switchLabel: { 38 | alignItems: 'center', 39 | flexDirection: 'row', 40 | }, 41 | label: { 42 | marginHorizontal: 5, 43 | fontSize: 16, 44 | }, 45 | }); 46 | 47 | export default Toolbar; 48 | -------------------------------------------------------------------------------- /examples/kanban/src/components/TaskList/AddTask.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useStoreActions, useStoreState } from '../../store'; 3 | import { StoreModel } from '../../store/model'; 4 | import generateId from '../../utils/generateId'; 5 | 6 | const AddTask: React.FC<{ list: keyof StoreModel }> = ({ list }) => { 7 | const [name, setName] = useState(''); 8 | const { name: listName } = useStoreState((state) => state[list]); 9 | const { addTask } = useStoreActions((actions) => actions[list]); 10 | 11 | return ( 12 |
{ 14 | e.preventDefault(); 15 | addTask({ id: generateId(), name }); 16 | setName(''); 17 | }} 18 | > 19 |