├── .babelrc
├── .circleci
└── config.yml
├── .dependabot
└── config.yml
├── .dockerignore
├── .github
└── ISSUE_TEMPLATE.md
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── README.md
├── __tests__
├── __snapshots__
│ └── reusable.test.js.snap
├── reusable.test.js
└── shallow-compare.js
├── docker-compose.yml
├── docs
└── basic-usage.md
├── examples
├── api
│ ├── .babelrc
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ ├── Starwars.js
│ │ ├── components
│ │ ├── Loader.js
│ │ └── Table.js
│ │ ├── index.js
│ │ ├── stores
│ │ ├── api.store.js
│ │ ├── characters.store.js
│ │ ├── loadingState.store.js
│ │ └── planets.store.js
│ │ └── style.css
├── basic-18
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ ├── Footer.js
│ │ ├── Header.js
│ │ ├── index.js
│ │ └── stores
│ │ └── counter.store.js
├── basic
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ ├── Footer.js
│ │ ├── Header.js
│ │ ├── index.js
│ │ └── stores
│ │ └── counter.store.js
├── lazy
│ ├── .env
│ ├── .gitignore
│ ├── README.md
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── logo192.png
│ │ ├── logo512.png
│ │ ├── manifest.json
│ │ └── robots.txt
│ ├── src
│ │ ├── App.css
│ │ ├── App.js
│ │ ├── Home.js
│ │ ├── Users.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── logo.svg
│ │ └── serviceWorker.js
│ └── yarn.lock
├── todomvc-parcel
│ ├── .babelrc
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ ├── Footer.js
│ │ ├── Header.js
│ │ ├── Todo.js
│ │ ├── Todos.js
│ │ ├── index.js
│ │ ├── stores
│ │ ├── filter.store.js
│ │ ├── todos-with-reducer.store.js
│ │ ├── todos.store.js
│ │ ├── todosSelectors.stores.js
│ │ └── usePersistedState.js
│ │ └── style.css
├── todomvc-redux
│ ├── .env
│ ├── .gitignore
│ ├── README.md
│ ├── config-overrides.js
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ └── manifest.json
│ └── src
│ │ ├── components
│ │ ├── Footer.js
│ │ ├── Header.js
│ │ ├── Todo.js
│ │ └── Todos.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── style.css
│ │ ├── todosConstants.js
│ │ ├── todosHooks.js
│ │ ├── todosReducer.js
│ │ └── todosSelectors.js
└── todomvc
│ ├── .env
│ ├── .gitignore
│ ├── README.md
│ ├── config-overrides.js
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
│ ├── src
│ ├── Footer.js
│ ├── Header.js
│ ├── Todo.js
│ ├── Todos.js
│ ├── index.css
│ ├── index.js
│ ├── serviceWorker.js
│ ├── stores
│ │ ├── filter.store.js
│ │ ├── todos-with-reducer.store.js
│ │ ├── todos.store.js
│ │ ├── todosSelectors.stores.js
│ │ └── usePersistedState.js
│ └── style.css
│ └── yarn.lock
├── jest.config.js
├── macro.js
├── package-lock.json
├── package.json
├── sandbox
├── .babelrc
├── index.html
├── package-lock.json
├── package.json
├── src
│ └── index.tsx
└── tsconfig.json
├── src
├── index.ts
├── react-reusable.tsx
├── reusable.ts
├── reuseable.macro.js
└── shallow-equal.ts
├── tsconfig.json
└── website
├── README.md
├── blog
├── 2019-04-05-docs-website.md
└── 2019-22-05-refactor-to-regular-hooks.md
├── core
└── Footer.js
├── i18n
└── en.json
├── package-lock.json
├── package.json
├── pages
└── en
│ ├── index.js
│ └── users.js
├── sidebars.json
├── siteConfig.js
└── static
├── css
└── custom.css
└── img
├── docusaurus.svg
├── favicon.png
├── favicon
└── favicon.ico
├── oss_logo.png
├── reusable.jpg
└── reusable.png
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"],
3 | "plugins": ["@babel/plugin-proposal-class-properties"]
4 | }
5 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/node:11.11
11 |
12 | # Specify service dependencies here if necessary
13 | # CircleCI maintains a library of pre-built images
14 | # documented at https://circleci.com/docs/2.0/circleci-images/
15 | # - image: circleci/mongo:3.4.4
16 |
17 | working_directory: ~/repo
18 |
19 | steps:
20 | - checkout
21 |
22 | # Download and cache dependencies
23 | - restore_cache:
24 | keys:
25 | - v1-dependencies-{{ checksum "package.json" }}
26 | # fallback to using the latest cache if no exact match is found
27 | - v1-dependencies-
28 |
29 | - run: npm install
30 |
31 | - save_cache:
32 | paths:
33 | - node_modules
34 | key: v1-dependencies-{{ checksum "package.json" }}
35 |
36 | # run tests!
37 | - run: npm run test
38 |
--------------------------------------------------------------------------------
/.dependabot/config.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | update_configs:
3 | - package_manager: "javascript"
4 | directory: "/"
5 | update_schedule: "live"
6 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | */node_modules
2 | *.log
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | PLEASE READ CAREFULLY!
2 |
3 | # Reproduce
4 | If reporting a bug, or requesting help, please reproduce here using the correct version:
5 | https://codesandbox.io/s/github/reusablejs/reusable/tree/master/examples/basic?fontsize=14&module=%2Fsrc%2Findex.js
6 |
7 | # Additional Info
8 | - paste your package.json
9 | - which browser
10 | - paste your code
11 |
12 | # Search Issues First
13 | Please look for an answer in the closed and open issues before submitting a new one.
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | ### macOS template
3 | # General
4 | .token
5 | .DS_Store
6 | .AppleDouble
7 | .LSOverride
8 |
9 | # Icon must end with two \r
10 | Icon
11 |
12 | # Thumbnails
13 | ._*
14 |
15 | # Files that might appear in the root of a volume
16 | .DocumentRevisions-V100
17 | .fseventsd
18 | .Spotlight-V100
19 | .TemporaryItems
20 | .Trashes
21 | .VolumeIcon.icns
22 | .com.apple.timemachine.donotpresent
23 |
24 | # Directories potentially created on remote AFP share
25 | .AppleDB
26 | .AppleDesktop
27 | Network Trash Folder
28 | Temporary Items
29 | .apdisk
30 | ### JetBrains template
31 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
32 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
33 |
34 | # User-specific stuff
35 | .idea/
36 | .idea/**/workspace.xml
37 | .idea/**/tasks.xml
38 | .idea/**/dictionaries
39 | .idea/**/shelf
40 |
41 | # Sensitive or high-churn files
42 | .idea/**/dataSources/
43 | .idea/**/dataSources.ids
44 | .idea/**/dataSources.local.xml
45 | .idea/**/sqlDataSources.xml
46 | .idea/**/dynamic.xml
47 | .idea/**/uiDesigner.xml
48 | .idea/**/dbnavigator.xml
49 |
50 | # Gradle
51 | .idea/**/gradle.xml
52 | .idea/**/libraries
53 |
54 | # CMake
55 | cmake-build-debug/
56 | cmake-build-release/
57 |
58 | # Mongo Explorer plugin
59 | .idea/**/mongoSettings.xml
60 |
61 | # File-based project format
62 | *.iws
63 |
64 | # IntelliJ
65 | out/
66 |
67 | # mpeltonen/sbt-idea plugin
68 | .idea_modules/
69 |
70 | # JIRA plugin
71 | atlassian-ide-plugin.xml
72 |
73 | # Cursive Clojure plugin
74 | .idea/replstate.xml
75 |
76 | # Crashlytics plugin (for Android Studio and IntelliJ)
77 | com_crashlytics_export_strings.xml
78 | crashlytics.properties
79 | crashlytics-build.properties
80 | fabric.properties
81 |
82 | # Editor-based Rest Client
83 | .idea/httpRequests
84 | ### VisualStudioCode template
85 | .vscode/*
86 | !.vscode/settings.json
87 | !.vscode/tasks.json
88 | !.vscode/launch.json
89 | !.vscode/extensions.json
90 | ### Node template
91 | # Logs
92 | logs
93 | *.log
94 | npm-debug.log*
95 | yarn-debug.log*
96 | yarn-error.log*
97 |
98 | # Runtime data
99 | pids
100 | *.pid
101 | *.seed
102 | *.pid.lock
103 |
104 | # Directory for instrumented libs generated by jscoverage/JSCover
105 | lib-cov
106 |
107 | # Coverage directory used by tools like istanbul
108 | coverage
109 |
110 | # nyc test coverage
111 | .nyc_output
112 |
113 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
114 | .grunt
115 |
116 | # Bower dependency directory (https://bower.io/)
117 | bower_components
118 |
119 | # node-waf configuration
120 | .lock-wscript
121 |
122 | # Compiled binary addons (https://nodejs.org/api/addons.html)
123 | build/Release
124 |
125 | # Dependency directories
126 | node_modules/
127 | jspm_packages/
128 |
129 | # TypeScript v1 declaration files
130 | typings/
131 |
132 | # Optional npm cache directory
133 | .npm
134 |
135 | # Optional eslint cache
136 | .eslintcache
137 |
138 | # Optional REPL history
139 | .node_repl_history
140 |
141 | # Output of 'npm pack'
142 | *.tgz
143 |
144 | # Yarn Integrity file
145 | .yarn-integrity
146 |
147 | # next.js build output
148 | .next
149 |
150 | dist/
151 | exampleDist/
152 | .rts2_cache_cjs/
153 | .rts2_cache_es/
154 | .rts2_cache_umd/
155 | .cache/
156 |
157 | website/build
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # 1.0.1 (March 3rd, 2020)
3 | * Added description, homepage, license, keywords to package json
4 |
5 |
6 | # 1.0.0 (Feb 17th, 2020)
7 | * Caching previously created components to avoid re-rendering them and losing store state
8 |
9 |
10 | # 1.0.0-alpha.13 (June 14th, 2019)
11 | * Fixed examples, renamed from units to stores
12 | * Added support for debugName for Macro
13 |
14 |
15 | # 1.0.0-alpha.12 (June 11th, 2019)
16 | * Version announced at ReactNext
17 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8.11.4
2 |
3 | WORKDIR /app/website
4 |
5 | EXPOSE 3000 35729
6 | COPY ./docs /app/docs
7 | COPY ./website /app/website
8 | RUN yarn install
9 |
10 | CMD ["yarn", "start"]
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://circleci.com/gh/reusablejs/reusable)
2 | [](https://badge.fury.io/js/reusable)
3 |
4 | # Reusable - state management with hooks
5 |
6 |
7 | - Use hooks to manage the store
8 | - One paradigm for both local and shared state, and an easier transition between the two
9 | - Use a single context provider and avoid nesting dozens of providers
10 | - Allow direct subscriptions with selectors for better re-render control
11 |
12 |
13 | # How to use
14 | Pass a custom hook to `createStore`:
15 |
16 | ```javascript
17 | const useCounter = createStore(() => {
18 | const [counter, setCounter] = useState(0);
19 | useEffect(...)
20 | const isOdd = useMemo(...);
21 |
22 | return {
23 | counter,
24 | isOdd,
25 | increment: () => setCounter(prev => prev + 1)
26 | decrement: () => setCounter(prev => prev - 1)
27 | }
28 | });
29 | ```
30 |
31 | and get a singleton store, with a hook that subscribes directly to that store:
32 | ```javascript
33 | const MyComponent = () => {
34 | const {counter, increment, decrement} = useCounter();
35 | }
36 |
37 | const AnotherComponent = () => {
38 | const {counter, increment, decrement} = useCounter(); // same counter
39 | }
40 | ```
41 |
42 | then wrap your app with a provider:
43 | ```javascript
44 | const App = () => (
45 |
46 | ...
47 |
48 | )
49 | ```
50 |
51 | Note there is no need to provide the store. Stores automatically plug into the top provider
52 |
53 | ## Selectors
54 | For better control over re-renders, use selectors:
55 |
56 | ```javascript
57 | const Comp1 = () => {
58 | const isOdd = useCounter(state => state.isOdd);
59 | }
60 | ```
61 | Comp1 will only re-render if counter switches between odd and even
62 |
63 | useCounter can take a second parameter that will override the comparison function (defaults to shallow compare):
64 | ```javascript
65 | const Comp1 = () => {
66 | const counter = useCounter(state => state, (prevValue, newValue) => prevValue === newValue);
67 | }
68 | ```
69 |
70 |
71 | ## Using stores from other stores
72 | Each store can use any other store similar to how components use them:
73 | ```javascript
74 | const useCurrentUser = createStore(() => ...);
75 | const usePosts = createStore(() => ...);
76 |
77 | const useCurrentUserPosts = createStore(() => {
78 | const currentUser = useCurrentUser();
79 | const posts = usePosts();
80 |
81 | return ...
82 | });
83 | ```
84 |
85 | # Demos
86 | **basic**
87 |
88 |
89 |
90 |
91 | **TodoMVC**
92 |
93 |
94 |
95 |
96 |
97 | # How does this compare to other state management solutions?
98 | Current state management solutions don't let you manage state using hooks, which causes you to manage local and global state differently, and have a costly transition between the two.
99 |
100 | Reusable solves this by seemingly transforming your custom hooks into global stores.
101 |
102 | ## What about hooks+Context?
103 | Using plain context has some drawbacks and limitations, that led us to write this library:
104 | - Context doesn't support selectors, render bailout, or debouncing
105 | - When managing global state using Context in a large app, you will probably have many small, single-purpose providers. Soon enough you'll find a Provider wrapper hell.
106 | - When you order the providers vertically, you can’t dynamically choose to depend on each other without changing the order, which might break things.
107 |
108 | # How does it work
109 | React hooks must run inside a component, and our store is based on a custom hook.
110 | So in order to have a store that uses a custom hook, we need to create a "host component" for each of our stores.
111 | The `ReusableProvider` component renders a `Stores` component, under which it will render one "host component" per store, which only runs the store's hook, and renders nothing to the DOM. Then, it uses an effect to update all subscribers with the new value.
112 | We use plain pubsub stores under the hood, and do shallowCompare on selector values to decide if we re-render the subscribing component or not.
113 |
114 | Notice that the `ReusableProvider` uses a Context provider at the top-level, but it provides a stable ref that never changes. This means that changing store values, and even dynamically adding stores won't re-render your app.
115 |
116 | ## Feedback / Contributing:
117 | We would love your feedback / suggestions
118 | Please open an issue for discussion before submitting a PR
119 | Thanks
120 |
--------------------------------------------------------------------------------
/__tests__/__snapshots__/reusable.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`reusable should fail without a provider 1`] = `"Error: Are you trying to use Reusable without a ReusableProvider?"`;
4 |
--------------------------------------------------------------------------------
/__tests__/reusable.test.js:
--------------------------------------------------------------------------------
1 | import {useState} from 'react'
2 | import {renderHook, act} from 'react-hooks-testing-library'
3 | import {getContainer, createContainer, replaceContainer} from '../src/reusable';
4 | import {createStore, ReusableProvider} from '../src/react-reusable';
5 |
6 | let container;
7 | let store;
8 | const value = {title: 'Yo'};
9 | const expectToBeAContainer = (object) => {
10 | expect(object.stores).toBeDefined();
11 | }
12 | const expectToBeAStore = (object) => {
13 | expect(object.cachedValue).toBeDefined();
14 | }
15 | describe('public container API', () => {
16 | beforeEach(() => {
17 | container = getContainer();
18 | });
19 | it('should allow to get a container', () => {
20 | expectToBeAContainer(container);
21 | });
22 | it('should allow to create a container', () => {
23 | expectToBeAContainer(createContainer());
24 | });
25 | it('should allow to replace the container', () => {
26 | const newContainer = createContainer();
27 | replaceContainer(newContainer);
28 | expect(getContainer()).toBe(newContainer);
29 | });
30 | it('should allow to create a store', () => {
31 | const fn = () => {};
32 | container.createStore(fn);
33 | expectToBeAStore(container.getStore(fn));
34 | });
35 | it('should allow to subscribe to creating a store', () => {
36 | const fn = () => {};
37 | let result;
38 | container.onStoresChanged(() => {
39 | result = container.getStore(fn);
40 | });
41 | container.createStore(fn);
42 | expectToBeAStore(result);
43 | });
44 | it('should not allow to get a store without creating it', () => {
45 | const fn = () => {};
46 |
47 | expect(() => container.getStore(fn)).toThrow();
48 | });
49 | it('should not allow to create a store twice', () => {
50 | const fn = () => {};
51 | container.createStore(fn);
52 | expect(() => container.createStore(fn)).toThrow();
53 | });
54 | it('should allow to unsubscribe from subscribing to creating a store', () => {
55 | const callback = jest.fn();
56 | const fn = () => {};
57 | const unsubscribe = container.onStoresChanged(callback);
58 | expect(callback.mock.calls.length).toBe(0);
59 | unsubscribe();
60 | container.createStore(fn);
61 | });
62 |
63 | });
64 | describe('public store API', () => {
65 | beforeEach(() => {
66 | const fn = () => value;
67 | container.createStore(fn);
68 | store = container.getStore(fn);
69 | });
70 |
71 | it('should allow to get the cached value', () => {
72 | expect(store.cachedValue).toBeDefined();
73 | });
74 |
75 | it('should allow to calculate the stores value', () => {
76 | expect(store.useValue()).toBe(value);
77 | });
78 |
79 | it('should cache the store value', () => {
80 | store.useValue();
81 | expect(store.cachedValue).toBe(value);
82 | });
83 | it('should allow to subscribe to value change', () => {
84 | const callback = jest.fn();
85 | store.subscribe(callback);
86 | store.useValue();
87 | store.notify();
88 | expect(callback.mock.calls.length).toBe(1);
89 | expect(callback.mock.calls[0][0]).toBe(value);
90 | });
91 | it('should allow to unsubscribe to value change', () => {
92 | const callback = jest.fn();
93 | const unsubscribe = store.subscribe(callback);
94 | expect(callback.mock.calls.length).toBe(0);
95 | store.useValue();
96 | unsubscribe();
97 | store.notify();
98 | });
99 | });
100 |
101 | describe('reusable', () => {
102 | beforeEach(() => {
103 | container = createContainer();
104 | replaceContainer(container);
105 | })
106 | it('should return a store value', () => {
107 | const useSomething = createStore(() => useState(1));
108 | const { result } = renderHook(useSomething, {
109 | wrapper: ReusableProvider
110 | });
111 | const [state] = result.current;
112 |
113 | expect(state).toBe(1);
114 | });
115 | it('should allow to set values on a store', () => {
116 | const useSomething = createStore(() => useState(1));
117 | const {result} = renderHook(useSomething, {
118 | wrapper: ReusableProvider
119 | });
120 | const [_, setState] = result.current;
121 |
122 | act(() => setState(3));
123 |
124 | const [state] = result.current;
125 |
126 | expect(state).toBe(3);
127 | });
128 | it('should allow to set values twice (stale state bug)', () => {
129 | const useSomething = createStore(() => useState(false));
130 | const {result} = renderHook(useSomething, {
131 | wrapper: ReusableProvider
132 | });
133 |
134 | const [_, setState] = result.current;
135 | act(() => setState(prev => !prev));
136 | let state = result.current[0];
137 | expect(state).toBe(true);
138 | act(() => setState(prev => !prev));
139 | state = result.current[0];
140 | expect(state).toBe(false);
141 | });
142 | it('should share state between stores', () => {
143 | const useSomething = createStore(() => useState(1));
144 | const {result} = renderHook(useSomething, {
145 | wrapper: ReusableProvider
146 | });
147 | const [_, setState] = result.current;
148 |
149 | const {result: result2} = renderHook(useSomething, {
150 | wrapper: ReusableProvider
151 | });
152 |
153 | act(() => setState(3));
154 | const [state] = result.current;
155 | expect(state).toBe(3);
156 |
157 | const [state2] = result2.current;
158 |
159 | expect(state2).toBe(3);
160 | });
161 | it('should fail without a provider', () => {
162 | const useSomething = createStore(() => useState(1));
163 | const {result} = renderHook(useSomething);
164 |
165 | expect(result.error.toString()).toMatchSnapshot();
166 | });
167 | it('should allow to return a function value', () => {
168 | const callback = jest.fn();
169 | const useSomething = createStore(() => callback);
170 | const {result} = renderHook(useSomething, {
171 | wrapper: ReusableProvider
172 | });
173 |
174 | expect(result.current).toBe(callback);
175 | expect(callback.mock.calls.length).toBe(0);
176 | });
177 | });
178 | describe('selectors', () => {
179 |
180 | it('should allow to use a selector', () => {
181 | const useSomething = createStore(() => useState(1));
182 | const useSelector = () => useSomething(state => state[0]);
183 | const { result } = renderHook(useSelector, {
184 | wrapper: ReusableProvider
185 | });
186 | const state = result.current;
187 |
188 | expect(state).toBe(1);
189 | });
190 |
191 | it('should allow to use a selector that returns a function', () => {
192 | const callback = jest.fn();
193 | const useSomething = createStore(() => ({ callback }));
194 | const useSelector = () => useSomething(state => state.callback);
195 | const {result} = renderHook(useSelector, {
196 | wrapper: ReusableProvider
197 | });
198 |
199 | expect(result.current).toBe(callback);
200 | expect(callback.mock.calls.length).toBe(0);
201 | });
202 |
203 | });
204 |
--------------------------------------------------------------------------------
/__tests__/shallow-compare.js:
--------------------------------------------------------------------------------
1 | import { shallowEqual } from '../src/shallow-equal';
2 |
3 | describe('util: shallow compare', () => {
4 |
5 | describe('should match by', () => {
6 | test('value', () => {
7 | const a = 'hello';
8 | const b = 'hello';
9 |
10 | expect(shallowEqual(a, b)).toEqual(true);
11 | });
12 |
13 | test('false value', () => {
14 | const a = false;
15 | const b = false;
16 |
17 | expect(shallowEqual(a, b)).toEqual(true);
18 | });
19 |
20 | test('null value', () => {
21 | const a = null;
22 | const b = null;
23 |
24 | expect(shallowEqual(a, b)).toEqual(true);
25 | });
26 |
27 | test('reference', () => {
28 | const a = { a: 1, b: 2 };
29 | const b = a;
30 |
31 | expect(shallowEqual(a, b)).toEqual(true);
32 | });
33 |
34 | test('shape', () => {
35 | const a = { a: 1, b: 2 };
36 | const b = { a: 1, b: 2 };
37 |
38 | expect(shallowEqual(a, b)).toEqual(true);
39 | });
40 |
41 | test('spread', () => {
42 | const a = { a: { c: 3 }, b: 2 };
43 | const b = { ...a };
44 |
45 | expect(shallowEqual(a, b)).toEqual(true);
46 | });
47 | });
48 |
49 | describe('should not match by', () => {
50 | test('different value', () => {
51 | const a = 'hello';
52 | const b = 'hello2';
53 |
54 | expect(shallowEqual(a, b)).toEqual(false);
55 | });
56 |
57 | test('different shape: should not match objects', () => {
58 | const a = { a: 1, b: 2 };
59 | const b = { a: 1 };
60 |
61 | expect(shallowEqual(a, b)).toEqual(false);
62 | });
63 | test('null & false should not match', () => {
64 | const a = null;
65 | const b = false;
66 |
67 | expect(shallowEqual(a, b)).toEqual(false);
68 | });
69 | test('true & false should not match', () => {
70 | const a = false;
71 | const b = true;
72 |
73 | expect(shallowEqual(a, b)).toEqual(false);
74 | });
75 |
76 | });
77 |
78 |
79 | });
80 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | docusaurus:
5 | build: .
6 | ports:
7 | - 3000:3000
8 | - 35729:35729
9 | volumes:
10 | - ./docs:/app/docs
11 | - ./website/blog:/app/website/blog
12 | - ./website/core:/app/website/core
13 | - ./website/i18n:/app/website/i18n
14 | - ./website/pages:/app/website/pages
15 | - ./website/static:/app/website/static
16 | - ./website/sidebars.json:/app/website/sidebars.json
17 | - ./website/siteConfig.js:/app/website/siteConfig.js
18 | working_dir: /app/website
19 |
--------------------------------------------------------------------------------
/docs/basic-usage.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: basic-usage
3 | title: Getting Started with Reusable
4 | sidebar_label: Getting Started
5 | ---
6 |
7 | [](https://circleci.com/gh/reusablejs/reusable)
8 | [](https://badge.fury.io/js/reusable)
9 |
10 | # Reusable - state management with hooks
11 |
12 |
13 | - Use hooks to manage the store
14 | - One paradigm for both local and shared state
15 | - Easier transition between the two
16 | - Use a single context provider and avoid nesting dozens of providers
17 | - Allow direct subscriptions with selectors for better re-render control
18 |
19 |
20 | # How to use
21 | Pass a custom hook to `createStore`:
22 |
23 | ```javascript
24 | const useCounter = createStore(() => {
25 | const [counter, setCounter] = useState(0);
26 | useEffect(...)
27 | const isOdd = useMemo(...);
28 |
29 | return {
30 | counter,
31 | isOdd,
32 | increment: () => setCounter(prev => prev + 1)
33 | decrement: () => setCounter(prev => prev - 1)
34 | }
35 | });
36 | ```
37 |
38 | and get a singleton store, with a hook that subscribes directly to that store:
39 | ```javascript
40 | const MyComponent = () => {
41 | const {counter, increment, decrement} = useCounter();
42 | }
43 |
44 | const AnotherComponent = () => {
45 | const {counter, increment, decrement} = useCounter(); // same counter
46 | }
47 | ```
48 |
49 | then wrap your app with a provider:
50 | ```javascript
51 | const App = () => (
52 |
53 | ...
54 |
55 | )
56 | ```
57 |
58 | Note there is no need to provide the store. Stores automatically plug into the top provider
59 |
60 | ## Selectors
61 | For better control over re-renders, use selectors:
62 |
63 | ```javascript
64 | const Comp1 = () => {
65 | const isOdd = useCounter(state => state.isOdd);
66 | }
67 | ```
68 | Comp1 will only re-render if counter switches between odd and even
69 |
70 | useCounter can take a second parameter that will override the comparison function (defaults to shallow compare):
71 | ```javascript
72 | const Comp1 = () => {
73 | const counter = useCounter(state => state, (prevValue, newValue) => prevValue === newValue);
74 | }
75 | ```
76 |
77 |
78 | ## Using stores from other stores
79 | Each store can use any other store similar to how components use them:
80 | ```javascript
81 | const useCurrentUser = createStore(() => ...);
82 | const usePosts = createStore(() => ...);
83 |
84 | const useCurrentUserPosts = createStore(() => {
85 | const currentUser = useCurrentUser();
86 | const posts = usePosts();
87 |
88 | return ...
89 | });
90 | ```
91 |
92 | # Demos
93 | **basic**
94 |
95 |
96 |
97 |
98 | **TodoMVC**
99 |
100 |
101 |
102 |
103 |
104 | # How does this compare to other state management solutions?
105 | Current state management solutions don't let you manage state using hooks, which causes you to manage local and global state differently, and have a costly transition between the two.
106 |
107 | Reusable solves this by seemingly transforming your custom hooks into global stores.
108 |
109 | ## What about hooks+Context?
110 | Using plain context has some drawbacks and limitations, that led us to write this library:
111 | - Context doesn't support selectors, render bailout, or debouncing
112 | - When managing global state using Context in a large app, you will probably have many small, single-purpose providers. Soon enough you'll find a Provider wrapper hell.
113 | - When you order the providers vertically, you can’t dynamically choose to depend on each other without changing the order, which might break things.
114 |
115 | # How does it work
116 | React hooks must run inside a component, and our store is based on a custom hook.
117 | So in order to have a store that uses a custom hook, we need to create a "host component" for each of our stores.
118 | The `ReusableProvider` component renders a `Stores` component, under which it will render one "host component" per store, which only runs the store's hook, and renders nothing to the DOM. Then, it uses an effect to update all subscribers with the new value.
119 | We use plain pubsub stores under the hood, and do shallowCompare on selector values to decide if we re-render the subscribing component or not.
120 |
121 | Notice that the `ReusableProvider` uses a Context provider at the top-level, but it provides a stable ref that never changes. This means that changing store values, and even dynamically adding stores won't re-render your app.
122 |
123 | ## Feedback / Contributing:
124 | We would love your feedback / suggestions
125 | Please open an issue for discussion before submitting a PR
126 | Thanks
127 |
--------------------------------------------------------------------------------
/examples/api/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": [
4 | ["@babel/transform-runtime"]
5 | ]
6 | }
--------------------------------------------------------------------------------
/examples/api/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reusable API example
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/examples/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "api",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "parcel ./index.html",
8 | "prepare": "../../node_modules/.bin/relative-deps"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "@babel/core": "^7.5.4",
14 | "@babel/plugin-proposal-class-properties": "^7.5.0",
15 | "@babel/plugin-transform-runtime": "^7.5.0",
16 | "@babel/runtime": "^7.5.4",
17 | "@babel/polyfill": "^7.4.4",
18 | "@babel/preset-react": "^7.0.0",
19 | "parcel": "^1.12.3"
20 | },
21 | "relativeDependencies": {
22 | "reusable": "../../"
23 | },
24 | "dependencies": {
25 | "react": "^16.8.6",
26 | "react-dom": "^16.8.6",
27 | "reusable": "^1.1.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/api/src/Starwars.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Table } from "./components/Table";
4 | import { Loader } from "./components/Loader";
5 | import { useCharacters } from "./stores/characters.store";
6 | import { useLoadingState } from "./stores/loadingState.store";
7 |
8 | export const Starwars = () => {
9 | const { characters } = useCharacters();
10 | const { loading } = useLoadingState();
11 | const columns = ["name", "gender", "birth_year", "height", "planet"];
12 | return (
13 |
14 | {loading || !characters.length ? (
15 |
16 | ) : (
17 |
18 | )}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/examples/api/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const Loader = props => {
4 | return (
5 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/examples/api/src/components/Table.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Loader } from "./Loader";
4 |
5 | export const Table = props => {
6 | const dataColumns = props.columns;
7 | const dataRows = props.data;
8 | const tableHeaders = (
9 |
10 |
11 | {dataColumns.map((column, key) => {
12 | return {column.toUpperCase()} | ;
13 | })}
14 |
15 |
16 | );
17 | const tableBody = dataRows.map((row, index) => {
18 | return (
19 |
20 | {dataColumns.map((column, key) => {
21 | return (
22 |
23 | {row[column] ? row[column] : }
24 | |
25 | );
26 | })}
27 |
28 | );
29 | });
30 | return (
31 |
32 | {tableHeaders}
33 | {tableBody}
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/examples/api/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { ReusableProvider } from "reusable";
4 |
5 | import "./style.css";
6 | import { Starwars } from "./Starwars";
7 |
8 | function App() {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
16 | const rootElement = document.getElementById("root");
17 | ReactDOM.render(, rootElement);
18 |
--------------------------------------------------------------------------------
/examples/api/src/stores/api.store.js:
--------------------------------------------------------------------------------
1 | import { createStore } from "reusable/dist/index";
2 | import { useLoadingState } from "./loadingState.store";
3 |
4 | export const useApi = createStore(() => {
5 | const { setLoadingState } = useLoadingState();
6 | const handleRequest = async (url, method, payload = {}, options = {}) => {
7 | const config = {
8 | ...options,
9 | method: method,
10 | headers: {
11 | "Content-type": "application/json; charset=UTF-8"
12 | }
13 | };
14 | if (method === "POST") {
15 | config.body = JSON.stringify(payload);
16 | }
17 | try {
18 | setLoadingState(true);
19 | const response = await fetch(url, config);
20 | const serverData = await response.json();
21 | setLoadingState(false);
22 | return serverData;
23 | } catch (e) {
24 | console.error(e);
25 | }
26 | };
27 | const get = (url, payload, options) => {
28 | return handleRequest(url, "GET", payload, options);
29 | };
30 |
31 | const put = (url, payload, options) => {
32 | return handleRequest(url, "PUT", payload, options);
33 | };
34 |
35 | const post = (url, payload, options) => {
36 | return handleRequest(url, "POST", payload, options);
37 | };
38 | return {
39 | get,
40 | post,
41 | put
42 | };
43 | });
44 |
--------------------------------------------------------------------------------
/examples/api/src/stores/characters.store.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { createStore } from "reusable";
3 |
4 | import { usePlanets } from "./planets.store";
5 | import { useApi } from "./api.store";
6 |
7 | export const useCharacters = createStore(() => {
8 | const [characters, setCharacters] = useState([]);
9 | const { get } = useApi();
10 | const {
11 | planets,
12 | initializePlanetEmptyMap,
13 | setAsyncPlanetData
14 | } = usePlanets();
15 | useEffect(() => {
16 | try {
17 | handleAsyncCharactersData();
18 | } catch (e) {
19 | console.error(e);
20 | }
21 | }, []);
22 |
23 | const handleAsyncCharactersData = async () => {
24 | const data = await get("https://swapi.co/api/people");
25 | const charactersData = data.results;
26 | setCharacters(charactersData);
27 | initializePlanetEmptyMap(charactersData);
28 | await setAsyncCharactersData(charactersData);
29 | };
30 |
31 | const setAsyncCharactersData = async data => {
32 | await setAsyncPlanetData();
33 | data.map(character => {
34 | character.planet = planets.get(character.homeworld);
35 | });
36 | setCharacters([...data]);
37 | };
38 | return {
39 | characters,
40 | setCharacters
41 | };
42 | });
43 |
--------------------------------------------------------------------------------
/examples/api/src/stores/loadingState.store.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { createStore } from "reusable";
3 |
4 | export const useLoadingState = createStore(() => {
5 | const [loading, setLoadingState] = useState(false);
6 | return {
7 | loading,
8 | setLoadingState
9 | };
10 | });
11 |
--------------------------------------------------------------------------------
/examples/api/src/stores/planets.store.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { createStore } from "reusable";
3 | import { useApi } from "./api.store";
4 |
5 | export const usePlanets = createStore(() => {
6 | const [planets, setPlanets] = useState(new Map());
7 | const { get } = useApi();
8 | const initializePlanetEmptyMap = data => {
9 | data.map(character => {
10 | if (!planets.has(character.homeworld)) {
11 | //setting an empty map of "planetUrl" => ""
12 | setPlanets(planets.set(character.homeworld, ""));
13 | }
14 | });
15 | };
16 | const setAsyncPlanetData = async () => {
17 | let promises = [];
18 | planets.forEach((value, key) => {
19 | promises.push(get(key));
20 | });
21 | const values = await Promise.all(promises);
22 | Object.keys(values).forEach(key => {
23 | setPlanets(planets.set(values[key].url, values[key].name));
24 | });
25 | };
26 | return {
27 | planets,
28 | setPlanets,
29 | initializePlanetEmptyMap,
30 | setAsyncPlanetData
31 | };
32 | });
33 |
--------------------------------------------------------------------------------
/examples/api/src/style.css:
--------------------------------------------------------------------------------
1 | .table {
2 | width: 100%;
3 | max-width: 100%;
4 | margin-bottom: 20px;
5 | }
6 |
7 | .table-hover > tbody tr:hover {
8 | background-color: #ffe30047;
9 | }
10 |
11 | tbody >tr>td, thead>tr>th{
12 | text-align: center;
13 | padding: 8px;
14 | line-height: 1.5;
15 | vertical-align: top;
16 | border-top: 1px solid lightgrey;
17 | }
18 |
19 | .table-delete-action {
20 | text-align: center;
21 | cursor: pointer;
22 | }
23 |
24 | html, body, #root, .container {
25 | width: 100%;
26 | height: 100%;
27 | margin: 0;
28 | padding: 0;
29 | }
30 |
31 | .container {
32 | display: flex;
33 | justify-content: center;
34 | align-items: center;
35 | background: #ffffff;
36 | }
37 |
38 | .loader {
39 | background: transparent;
40 | border-radius: 50%;
41 | border: 4px solid #ffe30047;
42 | border-right-color: black;
43 | animation: rotate 550ms infinite linear;
44 | }
45 |
46 | .loader.md {
47 | width: 100px;
48 | height: 100px;
49 | }
50 |
51 | .loader.sm {
52 | width: 15px;
53 | height: 15px;
54 | }
55 |
56 | @keyframes rotate {
57 | to { transform: rotate(1turn) }
58 | }
--------------------------------------------------------------------------------
/examples/basic-18/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
23 | React App
24 |
25 |
26 |
27 |
28 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/examples/basic-18/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "basic",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "parcel ./index.html",
8 | "prepare": "../../node_modules/.bin/relative-deps"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "@babel/core": "^7.4.3",
14 | "parcel": "1.12.3"
15 | },
16 | "relativeDependencies": {
17 | "reusable": "../../"
18 | },
19 | "dependencies": {
20 | "react": "18.2.0",
21 | "react-dom": "18.2.0",
22 | "reusable": "^1.1.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/basic-18/src/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {useCounter} from './stores/counter.store';
3 |
4 | export function Footer() {
5 | const [counter, setCounter] = useCounter();
6 |
7 | return (
8 |
9 | setCounter(e.target.value)}/>
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/examples/basic-18/src/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {useCounter} from './stores/counter.store';
3 |
4 | export function Header() {
5 | const [counter] = useCounter();
6 |
7 | return (
8 |
9 | {counter}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/examples/basic-18/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createRoot } from 'react-dom/client';
3 |
4 | import {ReusableProvider} from 'reusable';
5 | import {Header} from './Header';
6 | import {Footer} from './Footer';
7 |
8 | function App() {
9 | return (
10 |
11 |
12 | Counter in header and footer are reused
13 |
14 |
15 | );
16 | }
17 |
18 | const rootElement = createRoot(document.getElementById("root"));
19 | rootElement.render();
20 |
--------------------------------------------------------------------------------
/examples/basic-18/src/stores/counter.store.js:
--------------------------------------------------------------------------------
1 | import {useState} from 'react';
2 | import { createStore } from "reusable";
3 |
4 | export const useCounter = createStore(() => useState(0));
5 |
--------------------------------------------------------------------------------
/examples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
23 | React App
24 |
25 |
26 |
27 |
28 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/examples/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "basic",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "parcel ./index.html",
8 | "prepare": "../../node_modules/.bin/relative-deps"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "@babel/core": "^7.4.3",
14 | "parcel": "1.12.3"
15 | },
16 | "relativeDependencies": {
17 | "reusable": "../../"
18 | },
19 | "dependencies": {
20 | "react": "^16.8.6",
21 | "react-dom": "^16.8.6",
22 | "reusable": "^1.1.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/basic/src/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {useCounter} from './stores/counter.store';
3 |
4 | export function Footer() {
5 | const [counter, setCounter] = useCounter();
6 |
7 | return (
8 |
9 | setCounter(e.target.value)}/>
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/examples/basic/src/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {useCounter} from './stores/counter.store';
3 |
4 | export function Header() {
5 | const [counter] = useCounter();
6 |
7 | return (
8 |
9 | {counter}
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/examples/basic/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import {ReusableProvider} from 'reusable';
4 | import {Header} from './Header';
5 | import {Footer} from './Footer';
6 |
7 | function App() {
8 | return (
9 |
10 |
11 | Counter in header and footer are reused
12 |
13 |
14 | );
15 | }
16 |
17 | const rootElement = document.getElementById("root");
18 | ReactDOM.render(, rootElement);
19 |
--------------------------------------------------------------------------------
/examples/basic/src/stores/counter.store.js:
--------------------------------------------------------------------------------
1 | import {useState} from 'react';
2 | import { createStore } from "reusable";
3 |
4 | export const useCounter = createStore(() => useState(0));
5 |
--------------------------------------------------------------------------------
/examples/lazy/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
2 |
--------------------------------------------------------------------------------
/examples/lazy/.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 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/lazy/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `yarn build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/examples/lazy/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lazy",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.12.0",
7 | "react-dom": "^16.12.0",
8 | "react-router-dom": "^5.1.2",
9 | "react-scripts": "3.2.0",
10 | "reusable": "^1.1.0"
11 | },
12 | "relativeDependencies": {
13 | "reusable": "../../"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject",
20 | "prepare": "../../node_modules/.bin/relative-deps"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/examples/lazy/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reusablejs/reusable/0c24767dba77d445beb106f505ae1b631c0201ff/examples/lazy/public/favicon.ico
--------------------------------------------------------------------------------
/examples/lazy/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/lazy/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reusablejs/reusable/0c24767dba77d445beb106f505ae1b631c0201ff/examples/lazy/public/logo192.png
--------------------------------------------------------------------------------
/examples/lazy/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reusablejs/reusable/0c24767dba77d445beb106f505ae1b631c0201ff/examples/lazy/public/logo512.png
--------------------------------------------------------------------------------
/examples/lazy/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/examples/lazy/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/examples/lazy/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | }
8 |
9 | .App-header {
10 | background-color: #282c34;
11 | min-height: 100vh;
12 | display: flex;
13 | flex-direction: column;
14 | align-items: center;
15 | justify-content: center;
16 | font-size: calc(10px + 2vmin);
17 | color: white;
18 | }
19 |
20 | .App-link {
21 | color: #09d3ac;
22 | }
23 |
--------------------------------------------------------------------------------
/examples/lazy/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense, lazy } from 'react';
2 | import { ReusableProvider } from 'reusable';
3 | import './App.css';
4 |
5 | import {
6 | BrowserRouter as Router,
7 | Switch,
8 | Route,
9 | Link
10 | } from "react-router-dom";
11 |
12 | import Home from './Home';
13 |
14 | const Users = lazy(() => {
15 | const result = import('./Users');
16 | result.then(a => console.log(a));
17 | return result;
18 | });
19 |
20 | function App() {
21 | return (
22 |
23 |
24 |
34 | Loading...}>
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | export default App;
46 |
--------------------------------------------------------------------------------
/examples/lazy/src/Home.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createStore } from 'reusable';
3 | import { useState } from 'react';
4 |
5 | const useHome = createStore(() => useState('Home'));
6 |
7 | function Home() {
8 | const [title, setTitle] = useHome();
9 | return (
10 | {/* {arr.map((i) => )} */}
11 | setTitle(e.target.value)} />
12 |
);
13 | }
14 |
15 | export default Home;
16 |
--------------------------------------------------------------------------------
/examples/lazy/src/Users.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createStore } from 'reusable';
3 | import { useState } from 'react';
4 |
5 | const useUsers = createStore(() => useState('Users'));
6 |
7 | function Users() {
8 | const [title, setTitle] = useUsers();
9 |
10 | return setTitle(e.target.value)} />;
11 | }
12 |
13 | export default Users;
14 |
--------------------------------------------------------------------------------
/examples/lazy/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/examples/lazy/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/examples/lazy/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/lazy/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "@babel/plugin-proposal-class-properties"
8 | ]
9 | }
--------------------------------------------------------------------------------
/examples/todomvc-parcel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
23 | React App
24 |
25 |
26 |
27 |
28 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TodoMVC",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "parcel ./index.html",
8 | "prepare": "../../node_modules/.bin/relative-deps"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "@babel/core": "^7.4.3",
14 | "@babel/plugin-proposal-class-properties": "^7.4.0",
15 | "babel-plugin-reusable": "latest",
16 | "parcel": "^1.12.3"
17 | },
18 | "relativeDependencies": {
19 | "reusable": "../../"
20 | },
21 | "dependencies": {
22 | "react": "^16.8.6",
23 | "react-dom": "^16.8.6",
24 | "reusable": "^1.1.0",
25 | "uuid": "^3.3.2"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useFilter, FILTERS } from "./stores/filter.store";
3 | import { useTodos } from "./stores/todos.store";
4 | import { useTodosLeft } from "./stores/todosSelectors.stores";
5 |
6 | export function Footer() {
7 | const [filter, setFilter] = useFilter();
8 | const { clearCompleted } = useTodos()
9 | const todosLeft = useTodosLeft();
10 |
11 | return (
12 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/Header.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import { useTodos } from './stores/todos.store';
3 |
4 | export function Header() {
5 | const { addTodo } = useTodos();
6 | const [value, setValue] = useState('');
7 | const onChange = useCallback(e => setValue(e.target.value), []);
8 | const onKeyDown = useCallback((key) => {
9 | if (key.keyCode === 13) {
10 | addTodo(value);
11 | setValue('');
12 | }
13 | }, [value]);
14 |
15 | return (
16 |
26 | );
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/Todo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useTodos } from "./stores/todos.store";
3 |
4 | export function Todo({ todo }) {
5 | const { toggleTodo, removeTodo } = useTodos();
6 |
7 | return (
8 |
9 |
10 | toggleTodo(todo.id)} />
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/Todos.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useTodos } from './stores/todos.store';
3 | import { Todo } from './Todo';
4 | import { useIsAllSelected, useFilteredTodos } from "./stores/todosSelectors.stores";
5 |
6 | export function Todos() {
7 | const filteredTodos = useFilteredTodos();
8 | const { completeAll, setAllIncomplete } = useTodos();
9 | const isAllSelected = useIsAllSelected();
10 |
11 | return (
12 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { ReusableProvider } from "reusable";
4 | import { Header } from './Header';
5 | import { Todos } from './Todos';
6 | import { Footer } from './Footer';
7 |
8 | import './style.css';
9 |
10 | function App() {
11 | return (
12 |
13 |
18 |
19 | );
20 | }
21 |
22 | const rootElement = document.getElementById("root");
23 | ReactDOM.render(, rootElement);
24 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/stores/filter.store.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'reusable';
2 | import { usePersistedState } from './usePersistedState';
3 |
4 | export const FILTERS = {
5 | ALL: 'All',
6 | ACTIVE: 'Active',
7 | COMPLETED: 'Completed'
8 | };
9 |
10 | export const useFilter = createStore(() => usePersistedState('FILTER', 'ALL'));
11 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/stores/todos-with-reducer.store.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useReducer } from "react";
2 | import {createStore} from 'reusable';
3 | import { set, update, omit, mapValues, keyBy, omitBy, take } from 'lodash/fp';
4 | import uuid from 'uuid';
5 |
6 | const SET_TODOS = '[Todos] set todos';
7 | const ADD_TODO = '[Todos] add todo';
8 | const REMOVE_TODO = '[Todos] remove todo';
9 | const TOGGLE_TODO = '[Todos] toggle todo';
10 | const COMPLETE_ALL = '[Todos] complete all';
11 | const SET_ALL_INCOMPLETE = '[Todos] set all incomplete';
12 | const CLEAR_COMPLETED = '[Todos] clear completed';
13 |
14 | const todosReducer = (state, action) => {
15 | switch (action.type) {
16 | case SET_TODOS:
17 | return action.payload
18 |
19 | case ADD_TODO:
20 | return set(action.payload.id, action.payload, state);
21 |
22 | case REMOVE_TODO:
23 | return omit(action.payload.id, state)
24 |
25 | case TOGGLE_TODO:
26 | return update([action.payload.id, 'completed'], prev => !prev, state);
27 |
28 | case COMPLETE_ALL:
29 | return mapValues(set('completed', true), state)
30 |
31 | case SET_ALL_INCOMPLETE:
32 | return mapValues(set('completed', false), state)
33 |
34 | case CLEAR_COMPLETED:
35 | return omitBy(todo => todo.completed, state)
36 |
37 | default: return state;
38 | }
39 | }
40 |
41 | export const useTodos = createStore(() => {
42 | const [todos, dispatch] = useReducer(todosReducer, {});
43 |
44 | // fetch todos on start:
45 | useEffect(() => {
46 | fetch('https://jsonplaceholder.typicode.com/todos')
47 | .then(response => response.json())
48 | .then(todos => {
49 | const keyedTodos = keyBy(
50 | 'id',
51 | take(5, todos)
52 | );
53 | dispatch({ type: SET_TODOS, payload: keyedTodos });
54 | });
55 | }, []);
56 |
57 | // return object with action creators wrapped in dispatch:
58 | return {
59 | todos,
60 | addTodo: (title) => dispatch({
61 | type: ADD_TODO,
62 | payload: {
63 | id: uuid.v4(),
64 | title,
65 | completed: false
66 | }
67 | }),
68 | removeTodo: (id) => dispatch({ type: REMOVE_TODO, payload: { id } }),
69 | toggleTodo: (id) => dispatch({ type: TOGGLE_TODO, payload: { id } }),
70 | completeAll: () => dispatch({ type: COMPLETE_ALL }),
71 | setAllIncomplete: () => dispatch({ type: SET_ALL_INCOMPLETE }),
72 | clearCompleted: () => dispatch({ type: CLEAR_COMPLETED })
73 | };
74 | // We only need to notify changes if todos changed.
75 | // Alternatively, we could wrap each action with useCallback and skip the second param to Memo
76 | });
77 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/stores/todos.store.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { createStore } from "reusable";
3 | import { set, update, omit, mapValues, keyBy, omitBy, take } from 'lodash/fp';
4 | import uuid from 'uuid';
5 |
6 | export const useTodos = createStore(() => {
7 | const [todos, setTodos] = useState({});
8 |
9 | // fetch todos on start:
10 | useEffect(() => {
11 | fetch('https://jsonplaceholder.typicode.com/todos')
12 | .then(response => response.json())
13 | .then(data => {
14 | const keyedTodos = keyBy(
15 | 'id',
16 | take(5, data)
17 | );
18 | setTodos(keyedTodos);
19 | });
20 | }, []);
21 |
22 | // return object with actions:
23 | return {
24 | todos,
25 | addTodo: (title) => {
26 | const action = {
27 | id: uuid.v4(),
28 | title,
29 | completed: false
30 | };
31 | setTodos(set(action.id, action));
32 | },
33 | removeTodo: (id) => setTodos(omit(id)),
34 | toggleTodo: (id) => setTodos(update([id, 'completed'], prev => !prev)),
35 | completeAll: () => setTodos(mapValues(set('completed', true))),
36 | setAllIncomplete: () => setTodos(mapValues(set('completed', false))),
37 | clearCompleted: () => setTodos(omitBy(todo => todo.completed))
38 | };
39 | // We only need to notify changes if todos changed.
40 | // Alternatively, we could wrap each action with useCallback and skip the second param to Memo
41 | });
42 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/stores/todosSelectors.stores.js:
--------------------------------------------------------------------------------
1 | import { createStore } from "reusable";
2 | import { useMemo } from "react";
3 | import { values, every } from 'lodash/fp';
4 | import { useTodos } from "./todos.store";
5 | import { useFilter } from "./filter.store";
6 |
7 | export const useTodosArray = createStore(() => {
8 | const { todos } = useTodos();
9 |
10 | return useMemo(() => values(todos), [todos]);
11 | });
12 |
13 | export const useFilteredTodos = createStore(() => {
14 | const todosArray = useTodosArray();
15 | const [filter] = useFilter();
16 |
17 | return useMemo(() => todosArray.filter(todo => {
18 | if (filter === 'ACTIVE') {
19 | return !todo.completed;
20 | } else if (filter === 'COMPLETED') {
21 | return todo.completed
22 | } else {
23 | return true;
24 | }
25 | }), [filter, todosArray]);
26 | });
27 |
28 | export const useIsAllSelected = createStore(() => {
29 | const todosArray = useTodosArray();
30 |
31 | return every(todo => todo.completed, todosArray)
32 | });
33 |
34 | export const useTodosLeft = createStore(() => {
35 | const todosArray = useTodosArray();
36 |
37 | return todosArray.filter(todo => !todo.completed).length
38 | });
39 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/stores/usePersistedState.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | // custom hook:
4 | export const usePersistedState = (key, initialValue) => {
5 | const [value, setValue] = useState(() => {
6 | let persistedState = initialValue;
7 | try {
8 | persistedState = JSON.parse(localStorage.getItem(key));
9 | } catch (e) {
10 | console.error('cannot parse persisted state', e);
11 | }
12 | return persistedState;
13 | });
14 |
15 | useEffect(() => {
16 | try {
17 | localStorage.setItem(key, JSON.stringify(value));
18 | } catch (e) {
19 | console.error('cannot persist state', value);
20 | }
21 | }, [value, setValue]);
22 |
23 | return [value, setValue];
24 | }
25 |
--------------------------------------------------------------------------------
/examples/todomvc-parcel/src/style.css:
--------------------------------------------------------------------------------
1 | hr {
2 | margin: 20px 0;
3 | border: 0;
4 | border-top: 1px dashed #c5c5c5;
5 | border-bottom: 1px dashed #f7f7f7;
6 | }
7 |
8 | .learn a {
9 | font-weight: normal;
10 | text-decoration: none;
11 | color: #b83f45;
12 | }
13 |
14 | .learn a:hover {
15 | text-decoration: underline;
16 | color: #787e7e;
17 | }
18 |
19 | .learn h3,
20 | .learn h4,
21 | .learn h5 {
22 | margin: 10px 0;
23 | font-weight: 500;
24 | line-height: 1.2;
25 | color: #000;
26 | }
27 |
28 | .learn h3 {
29 | font-size: 24px;
30 | }
31 |
32 | .learn h4 {
33 | font-size: 18px;
34 | }
35 |
36 | .learn h5 {
37 | margin-bottom: 0;
38 | font-size: 14px;
39 | }
40 |
41 | .learn ul {
42 | padding: 0;
43 | margin: 0 0 30px 25px;
44 | }
45 |
46 | .learn li {
47 | line-height: 20px;
48 | }
49 |
50 | .learn p {
51 | font-size: 15px;
52 | font-weight: 300;
53 | line-height: 1.3;
54 | margin-top: 0;
55 | margin-bottom: 0;
56 | }
57 |
58 | #issue-count {
59 | display: none;
60 | }
61 |
62 | .quote {
63 | border: none;
64 | margin: 20px 0 60px 0;
65 | }
66 |
67 | .quote p {
68 | font-style: italic;
69 | }
70 |
71 | .quote p:before {
72 | content: '“';
73 | font-size: 50px;
74 | opacity: .15;
75 | position: absolute;
76 | top: -20px;
77 | left: 3px;
78 | }
79 |
80 | .quote p:after {
81 | content: '”';
82 | font-size: 50px;
83 | opacity: .15;
84 | position: absolute;
85 | bottom: -42px;
86 | right: 3px;
87 | }
88 |
89 | .quote footer {
90 | position: absolute;
91 | bottom: -40px;
92 | right: 0;
93 | }
94 |
95 | .quote footer img {
96 | border-radius: 3px;
97 | }
98 |
99 | .quote footer a {
100 | margin-left: 5px;
101 | vertical-align: middle;
102 | }
103 |
104 | .speech-bubble {
105 | position: relative;
106 | padding: 10px;
107 | background: rgba(0, 0, 0, .04);
108 | border-radius: 5px;
109 | }
110 |
111 | .speech-bubble:after {
112 | content: '';
113 | position: absolute;
114 | top: 100%;
115 | right: 30px;
116 | border: 13px solid transparent;
117 | border-top-color: rgba(0, 0, 0, .04);
118 | }
119 |
120 | .learn-bar > .learn {
121 | position: absolute;
122 | width: 272px;
123 | top: 8px;
124 | left: -300px;
125 | padding: 10px;
126 | border-radius: 5px;
127 | background-color: rgba(255, 255, 255, .6);
128 | transition-property: left;
129 | transition-duration: 500ms;
130 | }
131 |
132 | @media (min-width: 899px) {
133 | .learn-bar {
134 | width: auto;
135 | padding-left: 300px;
136 | }
137 |
138 | .learn-bar > .learn {
139 | left: 8px;
140 | }
141 | }
142 | html,
143 | body {
144 | margin: 0;
145 | padding: 0;
146 | }
147 |
148 | button {
149 | margin: 0;
150 | padding: 0;
151 | border: 0;
152 | background: none;
153 | font-size: 100%;
154 | vertical-align: baseline;
155 | font-family: inherit;
156 | font-weight: inherit;
157 | color: inherit;
158 | -webkit-appearance: none;
159 | appearance: none;
160 | -webkit-font-smoothing: antialiased;
161 | -moz-osx-font-smoothing: grayscale;
162 | }
163 |
164 | body {
165 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
166 | line-height: 1.4em;
167 | background: #f5f5f5;
168 | color: #4d4d4d;
169 | min-width: 230px;
170 | max-width: 550px;
171 | margin: 0 auto;
172 | -webkit-font-smoothing: antialiased;
173 | -moz-osx-font-smoothing: grayscale;
174 | font-weight: 300;
175 | }
176 |
177 | :focus {
178 | outline: 0;
179 | }
180 |
181 | .hidden {
182 | display: none;
183 | }
184 |
185 | .todoapp {
186 | background: #fff;
187 | margin: 130px 0 40px 0;
188 | position: relative;
189 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
190 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
191 | }
192 |
193 | .todoapp input::-webkit-input-placeholder {
194 | font-style: italic;
195 | font-weight: 300;
196 | color: #e6e6e6;
197 | }
198 |
199 | .todoapp input::-moz-placeholder {
200 | font-style: italic;
201 | font-weight: 300;
202 | color: #e6e6e6;
203 | }
204 |
205 | .todoapp input::input-placeholder {
206 | font-style: italic;
207 | font-weight: 300;
208 | color: #e6e6e6;
209 | }
210 |
211 | .todoapp h1 {
212 | position: absolute;
213 | top: -155px;
214 | width: 100%;
215 | font-size: 100px;
216 | font-weight: 100;
217 | text-align: center;
218 | color: rgba(175, 47, 47, 0.15);
219 | -webkit-text-rendering: optimizeLegibility;
220 | -moz-text-rendering: optimizeLegibility;
221 | text-rendering: optimizeLegibility;
222 | }
223 |
224 | .new-todo,
225 | .edit {
226 | position: relative;
227 | margin: 0;
228 | width: 100%;
229 | font-size: 24px;
230 | font-family: inherit;
231 | font-weight: inherit;
232 | line-height: 1.4em;
233 | border: 0;
234 | color: inherit;
235 | padding: 6px;
236 | border: 1px solid #999;
237 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
238 | box-sizing: border-box;
239 | -webkit-font-smoothing: antialiased;
240 | -moz-osx-font-smoothing: grayscale;
241 | }
242 |
243 | .new-todo {
244 | padding: 16px 16px 16px 60px;
245 | border: none;
246 | background: rgba(0, 0, 0, 0.003);
247 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
248 | }
249 |
250 | .main {
251 | position: relative;
252 | z-index: 2;
253 | border-top: 1px solid #e6e6e6;
254 | }
255 |
256 | .toggle-all {
257 | width: 1px;
258 | height: 1px;
259 | border: none; /* Mobile Safari */
260 | opacity: 0;
261 | position: absolute;
262 | right: 100%;
263 | bottom: 100%;
264 | }
265 |
266 | .toggle-all + label {
267 | width: 60px;
268 | height: 34px;
269 | font-size: 0;
270 | position: absolute;
271 | top: -52px;
272 | left: -13px;
273 | -webkit-transform: rotate(90deg);
274 | transform: rotate(90deg);
275 | }
276 |
277 | .toggle-all + label:before {
278 | content: '❯';
279 | font-size: 22px;
280 | color: #e6e6e6;
281 | padding: 10px 27px 10px 27px;
282 | }
283 |
284 | .toggle-all:checked + label:before {
285 | color: #737373;
286 | }
287 |
288 | .todo-list {
289 | margin: 0;
290 | padding: 0;
291 | list-style: none;
292 | }
293 |
294 | .todo-list li {
295 | position: relative;
296 | font-size: 24px;
297 | border-bottom: 1px solid #ededed;
298 | }
299 |
300 | .todo-list li:last-child {
301 | border-bottom: none;
302 | }
303 |
304 | .todo-list li.editing {
305 | border-bottom: none;
306 | padding: 0;
307 | }
308 |
309 | .todo-list li.editing .edit {
310 | display: block;
311 | width: 506px;
312 | padding: 12px 16px;
313 | margin: 0 0 0 43px;
314 | }
315 |
316 | .todo-list li.editing .view {
317 | display: none;
318 | }
319 |
320 | .todo-list li .toggle {
321 | text-align: center;
322 | width: 40px;
323 | /* auto, since non-WebKit browsers doesn't support input styling */
324 | height: auto;
325 | position: absolute;
326 | top: 0;
327 | bottom: 0;
328 | margin: auto 0;
329 | border: none; /* Mobile Safari */
330 | -webkit-appearance: none;
331 | appearance: none;
332 | }
333 |
334 | .todo-list li .toggle {
335 | opacity: 0;
336 | }
337 |
338 | .todo-list li .toggle + label {
339 | /*
340 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
341 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
342 | */
343 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
344 | background-repeat: no-repeat;
345 | background-position: center left;
346 | }
347 |
348 | .todo-list li .toggle:checked + label {
349 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
350 | }
351 |
352 | .todo-list li label {
353 | word-break: break-all;
354 | padding: 15px 15px 15px 60px;
355 | display: block;
356 | line-height: 1.2;
357 | transition: color 0.4s;
358 | }
359 |
360 | .todo-list li.completed label {
361 | color: #d9d9d9;
362 | text-decoration: line-through;
363 | }
364 |
365 | .todo-list li .destroy {
366 | display: none;
367 | position: absolute;
368 | top: 0;
369 | right: 10px;
370 | bottom: 0;
371 | width: 40px;
372 | height: 40px;
373 | margin: auto 0;
374 | font-size: 30px;
375 | color: #cc9a9a;
376 | margin-bottom: 11px;
377 | transition: color 0.2s ease-out;
378 | }
379 |
380 | .todo-list li .destroy:hover {
381 | color: #af5b5e;
382 | }
383 |
384 | .todo-list li .destroy:after {
385 | content: '×';
386 | }
387 |
388 | .todo-list li:hover .destroy {
389 | display: block;
390 | }
391 |
392 | .todo-list li .edit {
393 | display: none;
394 | }
395 |
396 | .todo-list li.editing:last-child {
397 | margin-bottom: -1px;
398 | }
399 |
400 | .footer {
401 | color: #777;
402 | padding: 10px 15px;
403 | height: 20px;
404 | text-align: center;
405 | border-top: 1px solid #e6e6e6;
406 | }
407 |
408 | .footer:before {
409 | content: '';
410 | position: absolute;
411 | right: 0;
412 | bottom: 0;
413 | left: 0;
414 | height: 50px;
415 | overflow: hidden;
416 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
417 | 0 8px 0 -3px #f6f6f6,
418 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
419 | 0 16px 0 -6px #f6f6f6,
420 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
421 | }
422 |
423 | .todo-count {
424 | float: left;
425 | text-align: left;
426 | }
427 |
428 | .todo-count strong {
429 | font-weight: 300;
430 | }
431 |
432 | .filters {
433 | margin: 0;
434 | padding: 0;
435 | list-style: none;
436 | position: absolute;
437 | right: 0;
438 | left: 0;
439 | }
440 |
441 | .filters li {
442 | display: inline;
443 | }
444 |
445 | .filters li a {
446 | color: inherit;
447 | margin: 3px;
448 | padding: 3px 7px;
449 | text-decoration: none;
450 | border: 1px solid transparent;
451 | border-radius: 3px;
452 | cursor: pointer;
453 | }
454 |
455 | .filters li a:hover {
456 | border-color: rgba(175, 47, 47, 0.1);
457 | }
458 |
459 | .filters li a.selected {
460 | border-color: rgba(175, 47, 47, 0.2);
461 | }
462 |
463 | .clear-completed,
464 | html .clear-completed:active {
465 | float: right;
466 | position: relative;
467 | line-height: 20px;
468 | text-decoration: none;
469 | cursor: pointer;
470 | }
471 |
472 | .clear-completed:hover {
473 | text-decoration: underline;
474 | }
475 |
476 | .info {
477 | margin: 65px auto 0;
478 | color: #bfbfbf;
479 | font-size: 10px;
480 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
481 | text-align: center;
482 | }
483 |
484 | .info p {
485 | line-height: 1;
486 | }
487 |
488 | .info a {
489 | color: inherit;
490 | text-decoration: none;
491 | font-weight: 400;
492 | }
493 |
494 | .info a:hover {
495 | text-decoration: underline;
496 | }
497 |
498 | /*
499 | Hack to remove background from Mobile Safari.
500 | Can't use it globally since it destroys checkboxes in Firefox
501 | */
502 | @media screen and (-webkit-min-device-pixel-ratio:0) {
503 | .toggle-all,
504 | .todo-list li .toggle {
505 | background: none;
506 | }
507 |
508 | .todo-list li .toggle {
509 | height: 40px;
510 | }
511 | }
512 |
513 | @media (max-width: 430px) {
514 | .footer {
515 | height: 50px;
516 | }
517 |
518 | .filters {
519 | bottom: 10px;
520 | }
521 | }
--------------------------------------------------------------------------------
/examples/todomvc-redux/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
2 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/.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 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 |
26 | .idea/
--------------------------------------------------------------------------------
/examples/todomvc-redux/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/config-overrides.js:
--------------------------------------------------------------------------------
1 | const { override } = require("customize-cra");
2 |
3 | module.exports = override();
4 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todomvc",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.8.6",
7 | "react-dom": "^16.8.6",
8 | "react-redux": "^7.1.0",
9 | "react-scripts": "2.1.8",
10 | "redux": "^4.0.2",
11 | "redux-devtools-extension": "^2.13.8",
12 | "reselect": "^4.0.0",
13 | "reusable": "^1.1.0",
14 | "uuid": "^3.3.2"
15 | },
16 | "relativeDependencies": {
17 | "reusable": "../../"
18 | },
19 | "scripts": {
20 | "start": "react-app-rewired start",
21 | "build": "react-app-rewired build",
22 | "test": "react-app-rewired test",
23 | "prepare": "../../node_modules/.bin/relative-deps",
24 | "eject": "react-app-rewired eject"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": [
30 | ">0.2%",
31 | "not dead",
32 | "not ie <= 11",
33 | "not op_mini all"
34 | ],
35 | "devDependencies": {
36 | "customize-cra": "^0.2.12",
37 | "react-app-rewired": "^2.1.1"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reusablejs/reusable/0c24767dba77d445beb106f505ae1b631c0201ff/examples/todomvc-redux/public/favicon.ico
--------------------------------------------------------------------------------
/examples/todomvc-redux/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 | React App
26 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { visibilityFilters } from "../todosConstants";
3 |
4 | import { useTodos, useFilterStore } from "../todosHooks";
5 |
6 | export function Footer() {
7 | const { filter, onFilterChange } = useFilterStore();
8 | const { activeTodosCount, clearCompleted } = useTodos();
9 |
10 | return (
11 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import { useTodo } from "../todosHooks";
3 |
4 | export function Header() {
5 | const { addTodo } = useTodo();
6 | const [value, setValue] = useState("");
7 | const onChange = useCallback(e => setValue(e.target.value), []);
8 | const onKeyDown = useCallback(
9 | key => {
10 | if (key.keyCode === 13) {
11 | addTodo(value);
12 | setValue("");
13 | }
14 | },
15 | [value]
16 | );
17 |
18 | return (
19 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useTodo } from "../todosHooks";
4 |
5 | export function Todo({ todo }) {
6 | const { toggleTodo, removeTodo } = useTodo();
7 |
8 | return (
9 |
10 |
11 | toggleTodo(todo.id)}
16 | />
17 |
18 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/components/Todos.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Todo } from "./Todo";
4 | import { useFilterTodosStore } from "../todosHooks";
5 |
6 | export function Todos() {
7 | const { filteredTodos } = useFilterTodosStore();
8 |
9 | return (
10 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { Provider } from "react-redux";
4 | import { ReusableProvider } from "reusable";
5 | import { composeWithDevTools } from "redux-devtools-extension";
6 | import { createStore, applyMiddleware, combineReducers } from "redux";
7 |
8 | import { todos } from "./todosReducer";
9 | import { Header } from "./components/Header";
10 | import { Todos } from "./components/Todos";
11 | import { Footer } from "./components/Footer";
12 |
13 | import "./style.css";
14 |
15 | const rootReducer = combineReducers({
16 | todos
17 | });
18 | const middleware = [];
19 | const store = createStore(
20 | rootReducer,
21 | composeWithDevTools(applyMiddleware(...middleware))
22 | );
23 |
24 | function App() {
25 | return (
26 |
27 |
28 |
33 |
34 |
35 | );
36 | }
37 |
38 | const rootElement = document.getElementById("root");
39 | ReactDOM.render(, rootElement);
40 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/style.css:
--------------------------------------------------------------------------------
1 | hr {
2 | margin: 20px 0;
3 | border: 0;
4 | border-top: 1px dashed #c5c5c5;
5 | border-bottom: 1px dashed #f7f7f7;
6 | }
7 |
8 | .learn a {
9 | font-weight: normal;
10 | text-decoration: none;
11 | color: #b83f45;
12 | }
13 |
14 | .learn a:hover {
15 | text-decoration: underline;
16 | color: #787e7e;
17 | }
18 |
19 | .learn h3,
20 | .learn h4,
21 | .learn h5 {
22 | margin: 10px 0;
23 | font-weight: 500;
24 | line-height: 1.2;
25 | color: #000;
26 | }
27 |
28 | .learn h3 {
29 | font-size: 24px;
30 | }
31 |
32 | .learn h4 {
33 | font-size: 18px;
34 | }
35 |
36 | .learn h5 {
37 | margin-bottom: 0;
38 | font-size: 14px;
39 | }
40 |
41 | .learn ul {
42 | padding: 0;
43 | margin: 0 0 30px 25px;
44 | }
45 |
46 | .learn li {
47 | line-height: 20px;
48 | }
49 |
50 | .learn p {
51 | font-size: 15px;
52 | font-weight: 300;
53 | line-height: 1.3;
54 | margin-top: 0;
55 | margin-bottom: 0;
56 | }
57 |
58 | #issue-count {
59 | display: none;
60 | }
61 |
62 | .quote {
63 | border: none;
64 | margin: 20px 0 60px 0;
65 | }
66 |
67 | .quote p {
68 | font-style: italic;
69 | }
70 |
71 | .quote p:before {
72 | content: '“';
73 | font-size: 50px;
74 | opacity: .15;
75 | position: absolute;
76 | top: -20px;
77 | left: 3px;
78 | }
79 |
80 | .quote p:after {
81 | content: '”';
82 | font-size: 50px;
83 | opacity: .15;
84 | position: absolute;
85 | bottom: -42px;
86 | right: 3px;
87 | }
88 |
89 | .quote footer {
90 | position: absolute;
91 | bottom: -40px;
92 | right: 0;
93 | }
94 |
95 | .quote footer img {
96 | border-radius: 3px;
97 | }
98 |
99 | .quote footer a {
100 | margin-left: 5px;
101 | vertical-align: middle;
102 | }
103 |
104 | .speech-bubble {
105 | position: relative;
106 | padding: 10px;
107 | background: rgba(0, 0, 0, .04);
108 | border-radius: 5px;
109 | }
110 |
111 | .speech-bubble:after {
112 | content: '';
113 | position: absolute;
114 | top: 100%;
115 | right: 30px;
116 | border: 13px solid transparent;
117 | border-top-color: rgba(0, 0, 0, .04);
118 | }
119 |
120 | .learn-bar > .learn {
121 | position: absolute;
122 | width: 272px;
123 | top: 8px;
124 | left: -300px;
125 | padding: 10px;
126 | border-radius: 5px;
127 | background-color: rgba(255, 255, 255, .6);
128 | transition-property: left;
129 | transition-duration: 500ms;
130 | }
131 |
132 | @media (min-width: 899px) {
133 | .learn-bar {
134 | width: auto;
135 | padding-left: 300px;
136 | }
137 |
138 | .learn-bar > .learn {
139 | left: 8px;
140 | }
141 | }
142 | html,
143 | body {
144 | margin: 0;
145 | padding: 0;
146 | }
147 |
148 | button {
149 | margin: 0;
150 | padding: 0;
151 | border: 0;
152 | background: none;
153 | font-size: 100%;
154 | vertical-align: baseline;
155 | font-family: inherit;
156 | font-weight: inherit;
157 | color: inherit;
158 | -webkit-appearance: none;
159 | appearance: none;
160 | -webkit-font-smoothing: antialiased;
161 | -moz-osx-font-smoothing: grayscale;
162 | }
163 |
164 | body {
165 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
166 | line-height: 1.4em;
167 | background: #f5f5f5;
168 | color: #4d4d4d;
169 | min-width: 230px;
170 | max-width: 550px;
171 | margin: 0 auto;
172 | -webkit-font-smoothing: antialiased;
173 | -moz-osx-font-smoothing: grayscale;
174 | font-weight: 300;
175 | }
176 |
177 | :focus {
178 | outline: 0;
179 | }
180 |
181 | .hidden {
182 | display: none;
183 | }
184 |
185 | .todoapp {
186 | background: #fff;
187 | margin: 130px 0 40px 0;
188 | position: relative;
189 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
190 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
191 | }
192 |
193 | .todoapp input::-webkit-input-placeholder {
194 | font-style: italic;
195 | font-weight: 300;
196 | color: #e6e6e6;
197 | }
198 |
199 | .todoapp input::-moz-placeholder {
200 | font-style: italic;
201 | font-weight: 300;
202 | color: #e6e6e6;
203 | }
204 |
205 | .todoapp input::input-placeholder {
206 | font-style: italic;
207 | font-weight: 300;
208 | color: #e6e6e6;
209 | }
210 |
211 | .todoapp h1 {
212 | position: absolute;
213 | top: -155px;
214 | width: 100%;
215 | font-size: 100px;
216 | font-weight: 100;
217 | text-align: center;
218 | color: rgba(175, 47, 47, 0.15);
219 | -webkit-text-rendering: optimizeLegibility;
220 | -moz-text-rendering: optimizeLegibility;
221 | text-rendering: optimizeLegibility;
222 | }
223 |
224 | .new-todo,
225 | .edit {
226 | position: relative;
227 | margin: 0;
228 | width: 100%;
229 | font-size: 24px;
230 | font-family: inherit;
231 | font-weight: inherit;
232 | line-height: 1.4em;
233 | border: 0;
234 | color: inherit;
235 | padding: 6px;
236 | border: 1px solid #999;
237 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
238 | box-sizing: border-box;
239 | -webkit-font-smoothing: antialiased;
240 | -moz-osx-font-smoothing: grayscale;
241 | }
242 |
243 | .new-todo {
244 | padding: 16px 16px 16px 60px;
245 | border: none;
246 | background: rgba(0, 0, 0, 0.003);
247 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
248 | }
249 |
250 | .main {
251 | position: relative;
252 | z-index: 2;
253 | border-top: 1px solid #e6e6e6;
254 | }
255 |
256 | .toggle-all {
257 | width: 1px;
258 | height: 1px;
259 | border: none; /* Mobile Safari */
260 | opacity: 0;
261 | position: absolute;
262 | right: 100%;
263 | bottom: 100%;
264 | }
265 |
266 | .toggle-all + label {
267 | width: 60px;
268 | height: 34px;
269 | font-size: 0;
270 | position: absolute;
271 | top: -52px;
272 | left: -13px;
273 | -webkit-transform: rotate(90deg);
274 | transform: rotate(90deg);
275 | }
276 |
277 | .toggle-all + label:before {
278 | content: '❯';
279 | font-size: 22px;
280 | color: #e6e6e6;
281 | padding: 10px 27px 10px 27px;
282 | }
283 |
284 | .toggle-all:checked + label:before {
285 | color: #737373;
286 | }
287 |
288 | .todo-list {
289 | margin: 0;
290 | padding: 0;
291 | list-style: none;
292 | }
293 |
294 | .todo-list li {
295 | position: relative;
296 | font-size: 24px;
297 | border-bottom: 1px solid #ededed;
298 | }
299 |
300 | .todo-list li:last-child {
301 | border-bottom: none;
302 | }
303 |
304 | .todo-list li.editing {
305 | border-bottom: none;
306 | padding: 0;
307 | }
308 |
309 | .todo-list li.editing .edit {
310 | display: block;
311 | width: 506px;
312 | padding: 12px 16px;
313 | margin: 0 0 0 43px;
314 | }
315 |
316 | .todo-list li.editing .view {
317 | display: none;
318 | }
319 |
320 | .todo-list li .toggle {
321 | text-align: center;
322 | width: 40px;
323 | /* auto, since non-WebKit browsers doesn't support input styling */
324 | height: auto;
325 | position: absolute;
326 | top: 0;
327 | bottom: 0;
328 | margin: auto 0;
329 | border: none; /* Mobile Safari */
330 | -webkit-appearance: none;
331 | appearance: none;
332 | }
333 |
334 | .todo-list li .toggle {
335 | opacity: 0;
336 | }
337 |
338 | .todo-list li .toggle + label {
339 | /*
340 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
341 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
342 | */
343 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
344 | background-repeat: no-repeat;
345 | background-position: center left;
346 | }
347 |
348 | .todo-list li .toggle:checked + label {
349 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
350 | }
351 |
352 | .todo-list li label {
353 | word-break: break-all;
354 | padding: 15px 15px 15px 60px;
355 | display: block;
356 | line-height: 1.2;
357 | transition: color 0.4s;
358 | }
359 |
360 | .todo-list li.completed label {
361 | color: #d9d9d9;
362 | text-decoration: line-through;
363 | }
364 |
365 | .todo-list li .destroy {
366 | display: none;
367 | position: absolute;
368 | top: 0;
369 | right: 10px;
370 | bottom: 0;
371 | width: 40px;
372 | height: 40px;
373 | margin: auto 0;
374 | font-size: 30px;
375 | color: #cc9a9a;
376 | margin-bottom: 11px;
377 | transition: color 0.2s ease-out;
378 | }
379 |
380 | .todo-list li .destroy:hover {
381 | color: #af5b5e;
382 | }
383 |
384 | .todo-list li .destroy:after {
385 | content: '×';
386 | }
387 |
388 | .todo-list li:hover .destroy {
389 | display: block;
390 | }
391 |
392 | .todo-list li .edit {
393 | display: none;
394 | }
395 |
396 | .todo-list li.editing:last-child {
397 | margin-bottom: -1px;
398 | }
399 |
400 | .footer {
401 | color: #777;
402 | padding: 10px 15px;
403 | height: 20px;
404 | text-align: center;
405 | border-top: 1px solid #e6e6e6;
406 | }
407 |
408 | .footer:before {
409 | content: '';
410 | position: absolute;
411 | right: 0;
412 | bottom: 0;
413 | left: 0;
414 | height: 50px;
415 | overflow: hidden;
416 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
417 | 0 8px 0 -3px #f6f6f6,
418 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
419 | 0 16px 0 -6px #f6f6f6,
420 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
421 | }
422 |
423 | .todo-count {
424 | float: left;
425 | text-align: left;
426 | }
427 |
428 | .todo-count strong {
429 | font-weight: 300;
430 | }
431 |
432 | .filters {
433 | margin: 0;
434 | padding: 0;
435 | list-style: none;
436 | position: absolute;
437 | right: 0;
438 | left: 0;
439 | }
440 |
441 | .filters li {
442 | display: inline;
443 | }
444 |
445 | .filters li a {
446 | color: inherit;
447 | margin: 3px;
448 | padding: 3px 7px;
449 | text-decoration: none;
450 | border: 1px solid transparent;
451 | border-radius: 3px;
452 | cursor: pointer;
453 | }
454 |
455 | .filters li a:hover {
456 | border-color: rgba(175, 47, 47, 0.1);
457 | }
458 |
459 | .filters li a.selected {
460 | border-color: rgba(175, 47, 47, 0.2);
461 | }
462 |
463 | .clear-completed,
464 | html .clear-completed:active {
465 | float: right;
466 | position: relative;
467 | line-height: 20px;
468 | text-decoration: none;
469 | cursor: pointer;
470 | }
471 |
472 | .clear-completed:hover {
473 | text-decoration: underline;
474 | }
475 |
476 | .info {
477 | margin: 65px auto 0;
478 | color: #bfbfbf;
479 | font-size: 10px;
480 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
481 | text-align: center;
482 | }
483 |
484 | .info p {
485 | line-height: 1;
486 | }
487 |
488 | .info a {
489 | color: inherit;
490 | text-decoration: none;
491 | font-weight: 400;
492 | }
493 |
494 | .info a:hover {
495 | text-decoration: underline;
496 | }
497 |
498 | /*
499 | Hack to remove background from Mobile Safari.
500 | Can't use it globally since it destroys checkboxes in Firefox
501 | */
502 | @media screen and (-webkit-min-device-pixel-ratio:0) {
503 | .toggle-all,
504 | .todo-list li .toggle {
505 | background: none;
506 | }
507 |
508 | .todo-list li .toggle {
509 | height: 40px;
510 | }
511 | }
512 |
513 | @media (max-width: 430px) {
514 | .footer {
515 | height: 50px;
516 | }
517 |
518 | .filters {
519 | bottom: 10px;
520 | }
521 | }
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/todosConstants.js:
--------------------------------------------------------------------------------
1 | let nextTodoId = 0;
2 | export const visibilityFilters = {
3 | ALL: "All",
4 | ACTIVE: "Active",
5 | COMPLETED: "Completed"
6 | };
7 |
8 | export const addTodo = text => ({
9 | type: "ADD_TODO",
10 | id: nextTodoId++,
11 | text
12 | });
13 |
14 | export const toggleTodo = id => ({
15 | type: "TOGGLE_TODO",
16 | id
17 | });
18 |
19 | export const removeTodo = id => ({
20 | type: "REMOVE_TODO",
21 | id
22 | });
23 |
24 | export const clearCompleted = () => ({
25 | type: "CLEAR_COMPLETED"
26 | });
27 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/todosHooks.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo, useState } from "react";
2 | import { useDispatch, useSelector, shallowEqual } from "react-redux";
3 | import { createStore } from "reusable";
4 |
5 | import {
6 | addTodo,
7 | toggleTodo,
8 | visibilityFilters,
9 | clearCompleted,
10 | removeTodo
11 | } from "./todosConstants";
12 | import { getActiveTodosCount, getAllTodos } from "./todosSelectors";
13 |
14 | export const useFilterStore = createStore(() => {
15 | const [filter, setFilter] = useState(visibilityFilters.ALL);
16 | return {
17 | filter,
18 | onFilterChange: setFilter
19 | };
20 | });
21 |
22 | export const useFilterTodosStore = createStore(() => {
23 | const { todos } = useTodos();
24 | //Using reusable store inside reusable store with redux hooks
25 | const { filter } = useFilterStore();
26 | return {
27 | filteredTodos: useMemo(
28 | () =>
29 | todos.filter(todo => {
30 | if (filter === "ACTIVE") {
31 | return !todo.completed;
32 | } else if (filter === "COMPLETED") {
33 | return todo.completed;
34 | } else {
35 | return true;
36 | }
37 | }),
38 | [filter, todos]
39 | )
40 | };
41 | });
42 |
43 | export const useTodo = () => {
44 | const dispatch = useDispatch();
45 | const addTodoItem = useCallback(
46 | todo => {
47 | dispatch(addTodo(todo));
48 | },
49 | [dispatch]
50 | );
51 | const toggleTodoItem = useCallback(
52 | id => {
53 | dispatch(toggleTodo(id));
54 | },
55 | [dispatch]
56 | );
57 | const removeTodoItem = useCallback(
58 | id => {
59 | dispatch(removeTodo(id));
60 | },
61 | [dispatch]
62 | );
63 | return {
64 | toggleTodo: toggleTodoItem,
65 | addTodo: addTodoItem,
66 | removeTodo: removeTodoItem
67 | };
68 | };
69 |
70 | export const useTodos = () => {
71 | const dispatch = useDispatch();
72 | const clearCompletedAction = useCallback(
73 | id => {
74 | dispatch(clearCompleted(id));
75 | },
76 | [dispatch]
77 | );
78 | const allTodos = useSelector(getAllTodos, shallowEqual);
79 | const activeTodosCount = useSelector(getActiveTodosCount, shallowEqual);
80 | return {
81 | todos: allTodos,
82 | activeTodosCount,
83 | clearCompleted: clearCompletedAction
84 | };
85 | };
86 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/todosReducer.js:
--------------------------------------------------------------------------------
1 | export const todos = (state = [], action) => {
2 | switch (action.type) {
3 | case "ADD_TODO":
4 | return [
5 | ...state,
6 | {
7 | id: action.id,
8 | text: action.text,
9 | completed: false
10 | }
11 | ];
12 | case "TOGGLE_TODO":
13 | return state.map(todo =>
14 | todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
15 | );
16 | case "CLEAR_COMPLETED":
17 | return state.filter(todo => !todo.completed);
18 | case "REMOVE_TODO":
19 | return state.filter(todo => action.id !== todo.id);
20 | default:
21 | return state;
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/examples/todomvc-redux/src/todosSelectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from "reselect";
2 |
3 | export const getAllTodos = state => state.todos;
4 | export const getActiveTodos = createSelector(
5 | getAllTodos,
6 | todos => todos.filter(t => !t.completed)
7 | );
8 | export const getActiveTodosCount = createSelector(
9 | getActiveTodos,
10 | todos => todos.length
11 | );
12 |
--------------------------------------------------------------------------------
/examples/todomvc/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
2 |
--------------------------------------------------------------------------------
/examples/todomvc/.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 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/todomvc/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `npm run build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
--------------------------------------------------------------------------------
/examples/todomvc/config-overrides.js:
--------------------------------------------------------------------------------
1 | const {
2 | override
3 | } = require("customize-cra");
4 |
5 | module.exports = override(
6 | );
7 |
--------------------------------------------------------------------------------
/examples/todomvc/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todomvc",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "react": "^16.8.6",
7 | "react-dom": "^16.8.6",
8 | "react-scripts": "2.1.8",
9 | "reusable": "^1.1.0",
10 | "uuid": "^3.3.2"
11 | },
12 | "relativeDependencies": {
13 | "reusable": "../../"
14 | },
15 | "scripts": {
16 | "start": "react-app-rewired start",
17 | "build": "react-app-rewired build",
18 | "test": "react-app-rewired test",
19 | "eject": "react-app-rewired eject",
20 | "prepare": "../../node_modules/.bin/relative-deps"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": [
26 | ">0.2%",
27 | "not dead",
28 | "not ie <= 11",
29 | "not op_mini all"
30 | ],
31 | "devDependencies": {
32 | "customize-cra": "^0.2.12",
33 | "react-app-rewired": "^2.1.1"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/examples/todomvc/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reusablejs/reusable/0c24767dba77d445beb106f505ae1b631c0201ff/examples/todomvc/public/favicon.ico
--------------------------------------------------------------------------------
/examples/todomvc/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 | React App
26 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/examples/todomvc/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/examples/todomvc/src/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useFilter, FILTERS } from "./stores/filter.store";
3 | import { useTodos } from "./stores/todos.store";
4 | import { useTodosLeft } from "./stores/todosSelectors.stores";
5 |
6 | export function Footer() {
7 | const [filter, setFilter] = useFilter();
8 | const { clearCompleted } = useTodos()
9 | const todosLeft = useTodosLeft();
10 |
11 | return (
12 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/examples/todomvc/src/Header.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import { useTodos } from './stores/todos.store';
3 |
4 | export function Header() {
5 | const { addTodo } = useTodos();
6 | const [value, setValue] = useState('');
7 | const onChange = useCallback(e => setValue(e.target.value), []);
8 | const onKeyDown = useCallback((key) => {
9 | if (key.keyCode === 13) {
10 | addTodo(value);
11 | setValue('');
12 | }
13 | }, [value]);
14 |
15 | return (
16 |
26 | );
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/examples/todomvc/src/Todo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useTodos } from "./stores/todos.store";
3 |
4 | export function Todo({ todo }) {
5 | const { toggleTodo, removeTodo } = useTodos();
6 |
7 | return (
8 |
9 |
10 | toggleTodo(todo.id)} />
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/examples/todomvc/src/Todos.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useTodos } from './stores/todos.store';
3 | import { Todo } from './Todo';
4 | import { useIsAllSelected, useFilteredTodos } from "./stores/todosSelectors.stores";
5 |
6 | export function Todos() {
7 | const filteredTodos = useFilteredTodos();
8 | const { completeAll, setAllIncomplete } = useTodos();
9 | const isAllSelected = useIsAllSelected();
10 |
11 | return (
12 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/examples/todomvc/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
6 | sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/examples/todomvc/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { ReusableProvider } from "reusable";
4 | import { Header } from './Header';
5 | import { Todos } from './Todos';
6 | import { Footer } from './Footer';
7 |
8 | import './style.css';
9 |
10 | function App() {
11 | return (
12 |
13 |
18 |
19 | );
20 | }
21 |
22 | const rootElement = document.getElementById("root");
23 | ReactDOM.render(, rootElement);
24 |
--------------------------------------------------------------------------------
/examples/todomvc/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/examples/todomvc/src/stores/filter.store.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'reusable';
2 | import { usePersistedState } from "./usePersistedState";
3 |
4 | export const FILTERS = {
5 | ALL: 'All',
6 | ACTIVE: 'Active',
7 | COMPLETED: 'Completed'
8 | };
9 |
10 | export const useFilter = createStore(() => usePersistedState('FILTER', 'ALL'));
11 |
--------------------------------------------------------------------------------
/examples/todomvc/src/stores/todos-with-reducer.store.js:
--------------------------------------------------------------------------------
1 | import { createStore } from "reusable";
2 | import { useEffect, useReducer } from "react";
3 | import { set, update, omit, mapValues, keyBy, omitBy, take } from 'lodash/fp';
4 | import uuid from 'uuid';
5 |
6 | const SET_TODOS = '[Todos] set todos';
7 | const ADD_TODO = '[Todos] add todo';
8 | const REMOVE_TODO = '[Todos] remove todo';
9 | const TOGGLE_TODO = '[Todos] toggle todo';
10 | const COMPLETE_ALL = '[Todos] complete all';
11 | const SET_ALL_INCOMPLETE = '[Todos] set all incomplete';
12 | const CLEAR_COMPLETED = '[Todos] clear completed';
13 |
14 | const todosReducer = (state, action) => {
15 | switch (action.type) {
16 | case SET_TODOS:
17 | return action.payload
18 |
19 | case ADD_TODO:
20 | return set(action.payload.id, action.payload, state);
21 |
22 | case REMOVE_TODO:
23 | return omit(action.payload.id, state)
24 |
25 | case TOGGLE_TODO:
26 | return update([action.payload.id, 'completed'], prev => !prev, state);
27 |
28 | case COMPLETE_ALL:
29 | return mapValues(set('completed', true), state)
30 |
31 | case SET_ALL_INCOMPLETE:
32 | return mapValues(set('completed', false), state)
33 |
34 | case CLEAR_COMPLETED:
35 | return omitBy(todo => todo.completed, state)
36 |
37 | default: return state;
38 | }
39 | }
40 |
41 | export const useTodos = createStore(() => {
42 | const [todos, dispatch] = useReducer(todosReducer, {});
43 |
44 | // fetch todos on start:
45 | useEffect(() => {
46 | fetch('https://jsonplaceholder.typicode.com/todos')
47 | .then(response => response.json())
48 | .then(todos => {
49 | const keyedTodos = keyBy(
50 | 'id',
51 | take(5, todos)
52 | );
53 | dispatch({ type: SET_TODOS, payload: keyedTodos });
54 | });
55 | }, []);
56 |
57 | // return object with action creators wrapped in dispatch:
58 | return {
59 | todos,
60 | addTodo: (title) => dispatch({
61 | type: ADD_TODO,
62 | payload: {
63 | id: uuid.v4(),
64 | title,
65 | completed: false
66 | }
67 | }),
68 | removeTodo: (id) => dispatch({ type: REMOVE_TODO, payload: { id } }),
69 | toggleTodo: (id) => dispatch({ type: TOGGLE_TODO, payload: { id } }),
70 | completeAll: () => dispatch({ type: COMPLETE_ALL }),
71 | setAllIncomplete: () => dispatch({ type: SET_ALL_INCOMPLETE }),
72 | clearCompleted: () => dispatch({ type: CLEAR_COMPLETED })
73 | };
74 | // We only need to notify changes if todos changed.
75 | // Alternatively, we could wrap each action with useCallback and skip the second param to Memo
76 | });
77 |
--------------------------------------------------------------------------------
/examples/todomvc/src/stores/todos.store.js:
--------------------------------------------------------------------------------
1 | import { createStore } from "reusable";
2 | import { useEffect, useState } from "react";
3 | import { set, update, omit, mapValues, keyBy, omitBy, take } from 'lodash/fp';
4 | import uuid from 'uuid';
5 |
6 | export const useTodos = createStore(() => {
7 | const [todos, setTodos] = useState({});
8 |
9 | // fetch todos on start:
10 | useEffect(() => {
11 | fetch('https://jsonplaceholder.typicode.com/todos')
12 | .then(response => response.json())
13 | .then(data => {
14 | const keyedTodos = keyBy(
15 | 'id',
16 | take(5, data)
17 | );
18 | setTodos(keyedTodos);
19 | });
20 | }, []);
21 |
22 | // return object with actions:
23 | return {
24 | todos,
25 | addTodo: (title) => {
26 | const action = {
27 | id: uuid.v4(),
28 | title,
29 | completed: false
30 | };
31 | setTodos(set(action.id, action));
32 | },
33 | removeTodo: (id) => setTodos(omit(id)),
34 | toggleTodo: (id) => setTodos(update([id, 'completed'], prev => !prev)),
35 | completeAll: () => setTodos(mapValues(set('completed', true))),
36 | setAllIncomplete: () => setTodos(mapValues(set('completed', false))),
37 | clearCompleted: () => setTodos(omitBy(todo => todo.completed))
38 | };
39 | // We only need to notify changes if todos changed.
40 | // Alternatively, we could wrap each action with useCallback and skip the second param to Memo
41 | });
42 |
--------------------------------------------------------------------------------
/examples/todomvc/src/stores/todosSelectors.stores.js:
--------------------------------------------------------------------------------
1 | import { createStore } from "reusable";
2 | import { useMemo } from "react";
3 | import { values, every } from 'lodash/fp';
4 | import { useTodos } from "./todos.store";
5 | import { useFilter } from "./filter.store";
6 |
7 | export const useTodosArray = createStore(() => {
8 | const { todos } = useTodos();
9 |
10 | return useMemo(() => values(todos), [todos]);
11 | });
12 |
13 | export const useFilteredTodos = createStore(() => {
14 | const todosArray = useTodosArray();
15 | const [filter] = useFilter();
16 |
17 | return useMemo(() => todosArray.filter(todo => {
18 | if (filter === 'ACTIVE') {
19 | return !todo.completed;
20 | } else if (filter === 'COMPLETED') {
21 | return todo.completed
22 | } else {
23 | return true;
24 | }
25 | }), [filter, todosArray]);
26 | });
27 |
28 | export const useIsAllSelected = createStore(() => {
29 | const todosArray = useTodosArray();
30 |
31 | return every(todo => todo.completed, todosArray)
32 | });
33 |
34 | export const useTodosLeft = createStore(() => {
35 | const todosArray = useTodosArray();
36 |
37 | return todosArray.filter(todo => !todo.completed).length
38 | });
39 |
--------------------------------------------------------------------------------
/examples/todomvc/src/stores/usePersistedState.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | // custom hook:
4 | export const usePersistedState = (key, initialValue) => {
5 | const [value, setValue] = useState(() => {
6 | let persistedState = initialValue;
7 | try {
8 | persistedState = JSON.parse(localStorage.getItem(key));
9 | } catch (e) {
10 | console.error('cannot parse persisted state', e);
11 | }
12 | return persistedState;
13 | });
14 |
15 | useEffect(() => {
16 | try {
17 | localStorage.setItem(key, JSON.stringify(value));
18 | } catch (e) {
19 | console.error('cannot persist state', value);
20 | }
21 | }, [value, setValue]);
22 |
23 | return [value, setValue];
24 | }
25 |
--------------------------------------------------------------------------------
/examples/todomvc/src/style.css:
--------------------------------------------------------------------------------
1 | hr {
2 | margin: 20px 0;
3 | border: 0;
4 | border-top: 1px dashed #c5c5c5;
5 | border-bottom: 1px dashed #f7f7f7;
6 | }
7 |
8 | .learn a {
9 | font-weight: normal;
10 | text-decoration: none;
11 | color: #b83f45;
12 | }
13 |
14 | .learn a:hover {
15 | text-decoration: underline;
16 | color: #787e7e;
17 | }
18 |
19 | .learn h3,
20 | .learn h4,
21 | .learn h5 {
22 | margin: 10px 0;
23 | font-weight: 500;
24 | line-height: 1.2;
25 | color: #000;
26 | }
27 |
28 | .learn h3 {
29 | font-size: 24px;
30 | }
31 |
32 | .learn h4 {
33 | font-size: 18px;
34 | }
35 |
36 | .learn h5 {
37 | margin-bottom: 0;
38 | font-size: 14px;
39 | }
40 |
41 | .learn ul {
42 | padding: 0;
43 | margin: 0 0 30px 25px;
44 | }
45 |
46 | .learn li {
47 | line-height: 20px;
48 | }
49 |
50 | .learn p {
51 | font-size: 15px;
52 | font-weight: 300;
53 | line-height: 1.3;
54 | margin-top: 0;
55 | margin-bottom: 0;
56 | }
57 |
58 | #issue-count {
59 | display: none;
60 | }
61 |
62 | .quote {
63 | border: none;
64 | margin: 20px 0 60px 0;
65 | }
66 |
67 | .quote p {
68 | font-style: italic;
69 | }
70 |
71 | .quote p:before {
72 | content: '“';
73 | font-size: 50px;
74 | opacity: .15;
75 | position: absolute;
76 | top: -20px;
77 | left: 3px;
78 | }
79 |
80 | .quote p:after {
81 | content: '”';
82 | font-size: 50px;
83 | opacity: .15;
84 | position: absolute;
85 | bottom: -42px;
86 | right: 3px;
87 | }
88 |
89 | .quote footer {
90 | position: absolute;
91 | bottom: -40px;
92 | right: 0;
93 | }
94 |
95 | .quote footer img {
96 | border-radius: 3px;
97 | }
98 |
99 | .quote footer a {
100 | margin-left: 5px;
101 | vertical-align: middle;
102 | }
103 |
104 | .speech-bubble {
105 | position: relative;
106 | padding: 10px;
107 | background: rgba(0, 0, 0, .04);
108 | border-radius: 5px;
109 | }
110 |
111 | .speech-bubble:after {
112 | content: '';
113 | position: absolute;
114 | top: 100%;
115 | right: 30px;
116 | border: 13px solid transparent;
117 | border-top-color: rgba(0, 0, 0, .04);
118 | }
119 |
120 | .learn-bar > .learn {
121 | position: absolute;
122 | width: 272px;
123 | top: 8px;
124 | left: -300px;
125 | padding: 10px;
126 | border-radius: 5px;
127 | background-color: rgba(255, 255, 255, .6);
128 | transition-property: left;
129 | transition-duration: 500ms;
130 | }
131 |
132 | @media (min-width: 899px) {
133 | .learn-bar {
134 | width: auto;
135 | padding-left: 300px;
136 | }
137 |
138 | .learn-bar > .learn {
139 | left: 8px;
140 | }
141 | }
142 | html,
143 | body {
144 | margin: 0;
145 | padding: 0;
146 | }
147 |
148 | button {
149 | margin: 0;
150 | padding: 0;
151 | border: 0;
152 | background: none;
153 | font-size: 100%;
154 | vertical-align: baseline;
155 | font-family: inherit;
156 | font-weight: inherit;
157 | color: inherit;
158 | -webkit-appearance: none;
159 | appearance: none;
160 | -webkit-font-smoothing: antialiased;
161 | -moz-osx-font-smoothing: grayscale;
162 | }
163 |
164 | body {
165 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
166 | line-height: 1.4em;
167 | background: #f5f5f5;
168 | color: #4d4d4d;
169 | min-width: 230px;
170 | max-width: 550px;
171 | margin: 0 auto;
172 | -webkit-font-smoothing: antialiased;
173 | -moz-osx-font-smoothing: grayscale;
174 | font-weight: 300;
175 | }
176 |
177 | :focus {
178 | outline: 0;
179 | }
180 |
181 | .hidden {
182 | display: none;
183 | }
184 |
185 | .todoapp {
186 | background: #fff;
187 | margin: 130px 0 40px 0;
188 | position: relative;
189 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
190 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
191 | }
192 |
193 | .todoapp input::-webkit-input-placeholder {
194 | font-style: italic;
195 | font-weight: 300;
196 | color: #e6e6e6;
197 | }
198 |
199 | .todoapp input::-moz-placeholder {
200 | font-style: italic;
201 | font-weight: 300;
202 | color: #e6e6e6;
203 | }
204 |
205 | .todoapp input::input-placeholder {
206 | font-style: italic;
207 | font-weight: 300;
208 | color: #e6e6e6;
209 | }
210 |
211 | .todoapp h1 {
212 | position: absolute;
213 | top: -155px;
214 | width: 100%;
215 | font-size: 100px;
216 | font-weight: 100;
217 | text-align: center;
218 | color: rgba(175, 47, 47, 0.15);
219 | -webkit-text-rendering: optimizeLegibility;
220 | -moz-text-rendering: optimizeLegibility;
221 | text-rendering: optimizeLegibility;
222 | }
223 |
224 | .new-todo,
225 | .edit {
226 | position: relative;
227 | margin: 0;
228 | width: 100%;
229 | font-size: 24px;
230 | font-family: inherit;
231 | font-weight: inherit;
232 | line-height: 1.4em;
233 | border: 0;
234 | color: inherit;
235 | padding: 6px;
236 | border: 1px solid #999;
237 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
238 | box-sizing: border-box;
239 | -webkit-font-smoothing: antialiased;
240 | -moz-osx-font-smoothing: grayscale;
241 | }
242 |
243 | .new-todo {
244 | padding: 16px 16px 16px 60px;
245 | border: none;
246 | background: rgba(0, 0, 0, 0.003);
247 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
248 | }
249 |
250 | .main {
251 | position: relative;
252 | z-index: 2;
253 | border-top: 1px solid #e6e6e6;
254 | }
255 |
256 | .toggle-all {
257 | width: 1px;
258 | height: 1px;
259 | border: none; /* Mobile Safari */
260 | opacity: 0;
261 | position: absolute;
262 | right: 100%;
263 | bottom: 100%;
264 | }
265 |
266 | .toggle-all + label {
267 | width: 60px;
268 | height: 34px;
269 | font-size: 0;
270 | position: absolute;
271 | top: -52px;
272 | left: -13px;
273 | -webkit-transform: rotate(90deg);
274 | transform: rotate(90deg);
275 | }
276 |
277 | .toggle-all + label:before {
278 | content: '❯';
279 | font-size: 22px;
280 | color: #e6e6e6;
281 | padding: 10px 27px 10px 27px;
282 | }
283 |
284 | .toggle-all:checked + label:before {
285 | color: #737373;
286 | }
287 |
288 | .todo-list {
289 | margin: 0;
290 | padding: 0;
291 | list-style: none;
292 | }
293 |
294 | .todo-list li {
295 | position: relative;
296 | font-size: 24px;
297 | border-bottom: 1px solid #ededed;
298 | }
299 |
300 | .todo-list li:last-child {
301 | border-bottom: none;
302 | }
303 |
304 | .todo-list li.editing {
305 | border-bottom: none;
306 | padding: 0;
307 | }
308 |
309 | .todo-list li.editing .edit {
310 | display: block;
311 | width: 506px;
312 | padding: 12px 16px;
313 | margin: 0 0 0 43px;
314 | }
315 |
316 | .todo-list li.editing .view {
317 | display: none;
318 | }
319 |
320 | .todo-list li .toggle {
321 | text-align: center;
322 | width: 40px;
323 | /* auto, since non-WebKit browsers doesn't support input styling */
324 | height: auto;
325 | position: absolute;
326 | top: 0;
327 | bottom: 0;
328 | margin: auto 0;
329 | border: none; /* Mobile Safari */
330 | -webkit-appearance: none;
331 | appearance: none;
332 | }
333 |
334 | .todo-list li .toggle {
335 | opacity: 0;
336 | }
337 |
338 | .todo-list li .toggle + label {
339 | /*
340 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
341 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
342 | */
343 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
344 | background-repeat: no-repeat;
345 | background-position: center left;
346 | }
347 |
348 | .todo-list li .toggle:checked + label {
349 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
350 | }
351 |
352 | .todo-list li label {
353 | word-break: break-all;
354 | padding: 15px 15px 15px 60px;
355 | display: block;
356 | line-height: 1.2;
357 | transition: color 0.4s;
358 | }
359 |
360 | .todo-list li.completed label {
361 | color: #d9d9d9;
362 | text-decoration: line-through;
363 | }
364 |
365 | .todo-list li .destroy {
366 | display: none;
367 | position: absolute;
368 | top: 0;
369 | right: 10px;
370 | bottom: 0;
371 | width: 40px;
372 | height: 40px;
373 | margin: auto 0;
374 | font-size: 30px;
375 | color: #cc9a9a;
376 | margin-bottom: 11px;
377 | transition: color 0.2s ease-out;
378 | }
379 |
380 | .todo-list li .destroy:hover {
381 | color: #af5b5e;
382 | }
383 |
384 | .todo-list li .destroy:after {
385 | content: '×';
386 | }
387 |
388 | .todo-list li:hover .destroy {
389 | display: block;
390 | }
391 |
392 | .todo-list li .edit {
393 | display: none;
394 | }
395 |
396 | .todo-list li.editing:last-child {
397 | margin-bottom: -1px;
398 | }
399 |
400 | .footer {
401 | color: #777;
402 | padding: 10px 15px;
403 | height: 20px;
404 | text-align: center;
405 | border-top: 1px solid #e6e6e6;
406 | }
407 |
408 | .footer:before {
409 | content: '';
410 | position: absolute;
411 | right: 0;
412 | bottom: 0;
413 | left: 0;
414 | height: 50px;
415 | overflow: hidden;
416 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
417 | 0 8px 0 -3px #f6f6f6,
418 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
419 | 0 16px 0 -6px #f6f6f6,
420 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
421 | }
422 |
423 | .todo-count {
424 | float: left;
425 | text-align: left;
426 | }
427 |
428 | .todo-count strong {
429 | font-weight: 300;
430 | }
431 |
432 | .filters {
433 | margin: 0;
434 | padding: 0;
435 | list-style: none;
436 | position: absolute;
437 | right: 0;
438 | left: 0;
439 | }
440 |
441 | .filters li {
442 | display: inline;
443 | }
444 |
445 | .filters li a {
446 | color: inherit;
447 | margin: 3px;
448 | padding: 3px 7px;
449 | text-decoration: none;
450 | border: 1px solid transparent;
451 | border-radius: 3px;
452 | cursor: pointer;
453 | }
454 |
455 | .filters li a:hover {
456 | border-color: rgba(175, 47, 47, 0.1);
457 | }
458 |
459 | .filters li a.selected {
460 | border-color: rgba(175, 47, 47, 0.2);
461 | }
462 |
463 | .clear-completed,
464 | html .clear-completed:active {
465 | float: right;
466 | position: relative;
467 | line-height: 20px;
468 | text-decoration: none;
469 | cursor: pointer;
470 | }
471 |
472 | .clear-completed:hover {
473 | text-decoration: underline;
474 | }
475 |
476 | .info {
477 | margin: 65px auto 0;
478 | color: #bfbfbf;
479 | font-size: 10px;
480 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
481 | text-align: center;
482 | }
483 |
484 | .info p {
485 | line-height: 1;
486 | }
487 |
488 | .info a {
489 | color: inherit;
490 | text-decoration: none;
491 | font-weight: 400;
492 | }
493 |
494 | .info a:hover {
495 | text-decoration: underline;
496 | }
497 |
498 | /*
499 | Hack to remove background from Mobile Safari.
500 | Can't use it globally since it destroys checkboxes in Firefox
501 | */
502 | @media screen and (-webkit-min-device-pixel-ratio:0) {
503 | .toggle-all,
504 | .todo-list li .toggle {
505 | background: none;
506 | }
507 |
508 | .todo-list li .toggle {
509 | height: 40px;
510 | }
511 | }
512 |
513 | @media (max-width: 430px) {
514 | .footer {
515 | height: 50px;
516 | }
517 |
518 | .filters {
519 | bottom: 10px;
520 | }
521 | }
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | "transform": {
4 | "^.+\\.jsx?$": "babel-jest", // Adding this line solved the issue
5 | "^.+\\.tsx?$": "ts-jest"
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/macro.js:
--------------------------------------------------------------------------------
1 | const macro = require('./src/reuseable.macro');
2 |
3 | module.exports = macro;
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reusable",
3 | "homepage": "https://github.com/reusablejs/reusable",
4 | "version": "1.1.0",
5 | "description": "Reusable is a library for state management using React hooks",
6 | "license": "MIT",
7 | "keywords": ["react", "hooks", "state management", "react hooks", "store"],
8 | "main": "dist/index.js",
9 | "source": "src/index.ts",
10 | "unpkg": "dist/reusable.umd.js",
11 | "types": "dist/index.d.ts",
12 | "dependencies": {
13 | "@babel/helper-module-imports": "^7.0.0",
14 | "babel-plugin-reusable": "1.0.0-alpha.8"
15 | },
16 | "peerDependencies": {
17 | "react": ">= 16.8",
18 | "react-dom": ">= 16.8"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "^7.4.0",
22 | "@babel/plugin-proposal-class-properties": "^7.4.4",
23 | "@babel/preset-env": "^7.4.2",
24 | "@babel/preset-react": "^7.0.0",
25 | "@types/jest": "^24.0.13",
26 | "@types/node": "^12.0.3",
27 | "@types/react": "18.0.0",
28 | "@types/react-dom": "18.0.0",
29 | "babel-plugin-macros": "^2.5.1",
30 | "babel-plugin-tester": "^6.0.1",
31 | "concurrently": "^4.1.0",
32 | "jest": "^24.8.0",
33 | "js-yaml": "^3.13.1",
34 | "lodash": "^4.17.11",
35 | "microbundle": "^0.15.1",
36 | "parcel": "^1.12.3",
37 | "prettier": "^1.16.4",
38 | "react": "^18.0.0",
39 | "react-dom": "^18.0.0",
40 | "react-hooks-testing-library": "^0.5.0",
41 | "react-test-renderer": "^16.8.6",
42 | "relative-deps": "^0.2.0",
43 | "ts-jest": "^24.0.2"
44 | },
45 | "scripts": {
46 | "example": "parcel ./example/index.html --out-dir exampleDist",
47 | "build": "microbundle --external all --jsx React.createElement",
48 | "build:dev": "microbundle watch --external all --jsx React.createElement",
49 | "sandbox": "cd sandbox && npm start",
50 | "dev": "concurrently 'npm run build:dev' 'npm run sandbox'",
51 | "docs:publish": "cd website && npm run build && USE_SSH=true npm run publish-gh-pages",
52 | "docs:dev": "cd website && npm start",
53 | "test": "jest",
54 | "prepublishOnly": "npm run build && npm test"
55 | },
56 | "files": [
57 | "dist/*",
58 | "src/reuseable.macro.js",
59 | "macro.js"
60 | ]
61 | }
62 |
--------------------------------------------------------------------------------
/sandbox/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "@babel/plugin-proposal-class-properties",
8 | "babel-plugin-reusable"
9 | ]
10 | }
--------------------------------------------------------------------------------
/sandbox/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
21 | React App
22 |
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/sandbox/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sandbox",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.ts",
6 | "scripts": {
7 | "start": "parcel ./index.html"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "devDependencies": {
12 | "@babel/core": "^7.4.3",
13 | "@babel/plugin-proposal-class-properties": "^7.4.0",
14 | "parcel": "^1.12.3"
15 | },
16 | "dependencies": {
17 | "@types/react": "^16.8.19",
18 | "@types/react-dom": "^16.8.4"
19 | }
20 | }
--------------------------------------------------------------------------------
/sandbox/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import ReactDOM from "react-dom";
3 | import { ReusableProvider, createStore } from "../../dist";
4 |
5 | const useCounter = createStore(function counter() {
6 | return useState(0);
7 | });
8 |
9 | const useStep = createStore(function step() {
10 | return useState(0);
11 | });
12 |
13 | const useFnObject = createStore(function fnObject() {
14 | const [_, setState] = useState(0);
15 |
16 | const callback = () => {
17 | console.log('callback')
18 | setState(10);
19 | }
20 | return {
21 | callback
22 | };
23 | });
24 |
25 | const useMultiply = createStore(function multiply() {
26 | const [counter] = useCounter();
27 | const [step] = useStep();
28 |
29 | return counter * step;
30 | })
31 |
32 | function Header() {
33 | const [counter, setCounter] = useCounter();
34 | const [step, setStep] = useStep();
35 |
36 | return (
37 |
38 | Counter: { counter }
39 |
40 |
41 |
42 | Step: { step }
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
50 | function Footer() {
51 | const counter = useCounter(([val]) => val, (a, b) => Math.abs(a - b) < 2);
52 | const step = useStep(([val]) => Math.floor(val / 4));
53 |
54 | return (
55 |
59 | );
60 | }
61 |
62 | const Multiply = () => {
63 | return { useMultiply() } ;
64 | }
65 |
66 | const App = () => {
67 | const [showMultiply, setShowMultiply] = useState(true);
68 | const callback = useFnObject(state => state.callback);
69 | setTimeout(() => callback(), 1000);
70 |
71 | return (
72 |
73 |
74 | { showMultiply ? : null }
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | const rootElement = document.getElementById("root");
82 | ReactDOM.render(, rootElement);
83 |
--------------------------------------------------------------------------------
/sandbox/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "declaration": true,
5 | "outDir": "dist",
6 | "strict": true,
7 | "allowSyntheticDefaultImports": true,
8 | "sourceMap": true,
9 | "jsx": "react",
10 | "lib": [
11 | "dom",
12 | "es5",
13 | "es2015.promise"
14 | ]
15 | },
16 | "include": [
17 | "src/**/*"
18 | ],
19 | "exclude": [
20 | "node_modules"
21 | ]
22 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './reusable';
2 | export * from './react-reusable';
3 |
--------------------------------------------------------------------------------
/src/react-reusable.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FC, useState, useContext, useEffect, useRef } from 'react';
3 | import { shallowEqual, AreEqual } from './shallow-equal';
4 | import {
5 | Container,
6 | getContainer,
7 | Store as StoreClass,
8 | HookFn,
9 | } from './reusable';
10 |
11 | const ReusableContext = React.createContext(null);
12 |
13 | export const ReusableProvider: FC<{ children?: React.ReactNode }> = ({
14 | children,
15 | }) => (
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 | );
23 | Object.defineProperty(ReusableProvider, 'displayName', {
24 | value: 'ReusableProvider',
25 | });
26 |
27 | const componentCache = new Map();
28 |
29 | const createStoreComponent = (store: StoreClass) => {
30 | if (!componentCache.has(store)) {
31 | const Component = React.memo(function StoreComponent() {
32 | store.useValue();
33 |
34 | // eslint-disable-next-line react-hooks/exhaustive-deps
35 | useEffect(() => store.notify(), [store.cachedValue]);
36 |
37 | return null;
38 | });
39 |
40 | Object.defineProperty(Component, 'name', { value: store.name });
41 |
42 | componentCache.set(store, Component);
43 | }
44 |
45 | return componentCache.get(store);
46 | };
47 |
48 | const useContainer = () => {
49 | const container = useContext(ReusableContext) as Container;
50 |
51 | if (container === null) {
52 | throw new Error(
53 | 'Are you trying to use Reusable without a ReusableProvider?'
54 | );
55 | }
56 |
57 | return container;
58 | };
59 | const Stores = () => {
60 | const container = useContainer();
61 | const [stores, setStores] = useState(() => container.getStoresArray());
62 |
63 | useEffect(() => {
64 | return container.onStoresChanged(() => {
65 | setStores(container.getStoresArray());
66 | });
67 | // eslint-disable-next-line react-hooks/exhaustive-deps
68 | }, []);
69 | return (
70 |
71 | {stores.map((store: StoreClass, index: number) => {
72 | const StoreComponent = createStoreComponent(store);
73 |
74 | return ;
75 | })}
76 |
77 | );
78 | };
79 | Object.defineProperty(Stores, 'displayName', { value: 'Stores' });
80 |
81 | type SelectorFn = (val: HookValue) => SelectorValue;
82 | const identity = (val: any) => val;
83 |
84 | // eslint-disable-next-line max-params
85 | function useStore(
86 | fn: HookFn,
87 | selector: SelectorFn,
88 | areEqual: AreEqual
89 | ) {
90 | const store = useContainer().getStore(fn);
91 | React.useDebugValue('reusable');
92 | const [localCopy, setLocalCopy] = useState(() =>
93 | selector(store.getCachedValue())
94 | );
95 | const localCopyRef = useRef(localCopy);
96 | localCopyRef.current = localCopy;
97 | useEffect(() => {
98 | return store.subscribe((newValue) => {
99 | const selectedNewValue = selector(newValue);
100 |
101 | if (!areEqual(selectedNewValue, localCopyRef.current)) {
102 | setLocalCopy(() => selectedNewValue);
103 | }
104 | });
105 | }, [store, selector, areEqual]);
106 |
107 | return localCopy;
108 | }
109 |
110 | export function createStore(fn: HookFn) {
111 | const store = getContainer().createStore(fn);
112 |
113 | // overload return function:
114 | function useStoreHook(): HookValue;
115 |
116 | function useStoreHook(
117 | selector?: SelectorFn,
118 | areEqual?: AreEqual
119 | ): SelectorValue;
120 |
121 | function useStoreHook(
122 | selector?: SelectorFn,
123 | areEqual?: AreEqual
124 | ) {
125 | React.useDebugValue(store.name);
126 |
127 | return useStore(fn, selector || identity, areEqual || shallowEqual);
128 | }
129 | return useStoreHook;
130 | }
131 |
132 | // TBD:
133 | // export const reusableReducer = (reducer, initialValue, init, options) => {
134 | // const fn = () => useReducer(reducer, initialValue, init);
135 |
136 | // getStore().createUnit(fn, options);
137 | // return (selector, areEqual) => useStore(fn, selector, areEqual);
138 | // };
139 |
140 | // export const reusableState = (init, options) => {
141 | // const fn = () => useState(init);
142 |
143 | // getStore().createUnit(fn, options);
144 | // return (selector, areEqual) => useStore(fn, selector, areEqual);
145 | // };
146 |
--------------------------------------------------------------------------------
/src/reusable.ts:
--------------------------------------------------------------------------------
1 | export type HookFn = () => HookValue;
2 | export type StoreValueChangeCallback = (value: HookValue) => void;
3 | export type StoresChangeCallback = () => void;
4 |
5 | export class Store {
6 | name: string;
7 | subscribers: StoreValueChangeCallback[] = [];
8 | cachedValue: HookValue | null = null;
9 | constructor(private fn: HookFn) {
10 | this.name = fn.name || 'Store';
11 | }
12 |
13 | getCachedValue() {
14 | return this.cachedValue as HookValue;
15 | }
16 |
17 | useValue() {
18 | this.cachedValue = this.fn();
19 |
20 | return this.cachedValue as HookValue;
21 | }
22 |
23 | subscribe(callback: StoreValueChangeCallback) {
24 | this.subscribers = [...this.subscribers, callback];
25 | return () => {
26 | this.subscribers = this.subscribers.filter(sub => sub !== callback)
27 | }
28 | }
29 |
30 | notify() {
31 | this.subscribers.forEach(sub => sub(this.cachedValue as HookValue));
32 | }
33 | }
34 |
35 | export class Container {
36 | stores = new Map();
37 | subscribers: StoresChangeCallback[] = [];
38 | onStoresChanged(callback: StoresChangeCallback) {
39 | this.subscribers = [...this.subscribers, callback];
40 | return () => {
41 | this.subscribers = this.subscribers.filter(item => item !== callback);
42 | }
43 | }
44 | createStore(fn: HookFn) {
45 | if (this.stores.has(fn)) {
46 | throw new Error('Store already exist');
47 | }
48 | const store = new Store(fn);
49 | this.stores.set(fn, store);
50 | this.notifyStoresChanged();
51 |
52 | return store;
53 | }
54 | getStore(fn: HookFn):Store {
55 | if (!this.stores.has(fn)) {
56 | throw new Error('Store doesn\'t exist');
57 | }
58 | return >this.stores.get(fn);
59 | }
60 | notifyStoresChanged() {
61 | this.subscribers.forEach(sub => sub());
62 | }
63 | getStoresArray() {
64 | const storesArray: Store[] = [];
65 |
66 | this.stores.forEach((store) => {
67 | storesArray.push(store);
68 | })
69 | return storesArray;
70 | }
71 | }
72 | export const createContainer = () => new Container();
73 | let defaultContainer = new Container();
74 | export const getContainer = () => defaultContainer;
75 | export const replaceContainer = (mockedContainer: Container) => defaultContainer = mockedContainer;
76 |
--------------------------------------------------------------------------------
/src/reuseable.macro.js:
--------------------------------------------------------------------------------
1 | const babelPluginMacros = require('babel-plugin-macros');
2 | const createMacro = babelPluginMacros.createMacro;
3 | const MacroError = babelPluginMacros.MacroError;
4 |
5 | const babelPlugin = require('babel-plugin-reusable').default;
6 |
7 | const babelHelperModuleImports = require('@babel/helper-module-imports');
8 | const addDefault = babelHelperModuleImports.addDefault;
9 | const addNamed = babelHelperModuleImports.addNamed;
10 |
11 | const allowedImports = ['createStore'];
12 |
13 | function reuseableMacro({ references, state, babel: { types: t }, config = {} }) {
14 | const program = state.file.path;
15 |
16 | // FIRST STEP : replace `reusable/macro` by `reusable
17 | // references looks like this
18 | // { default: [path, path], css: [path], ... }
19 | let customImportName;
20 | Object.keys(references).forEach(refName => {
21 | if (!allowedImports.includes(refName)) {
22 | throw new MacroError(
23 | `Invalid import: ${refName}. You can only import ${allowedImports.join(
24 | ', '
25 | )} from 'reusable/macro'.`
26 | );
27 | }
28 |
29 | // generate new identifier
30 | let id;
31 | if (refName === 'default') {
32 | id = addDefault(program, 'reusable', { nameHint: 'reusable' });
33 | customImportName = id;
34 | } else {
35 | id = addNamed(program, refName, 'reusable', { nameHint: refName });
36 | }
37 |
38 | // update references with the new identifiers
39 | references[refName].forEach(referencePath => {
40 | // eslint-disable-next-line no-param-reassign
41 | referencePath.node.name = id.name;
42 | });
43 | });
44 |
45 | // SECOND STEP : apply babel-plugin-reusable to the file
46 | const stateWithOpts = { ...state, opts: config, customImportName };
47 | program.traverse(babelPlugin({ types: t }).visitor, stateWithOpts);
48 | }
49 |
50 | module.exports = createMacro(reuseableMacro);
51 |
--------------------------------------------------------------------------------
/src/shallow-equal.ts:
--------------------------------------------------------------------------------
1 | const hasOwn = Object.prototype.hasOwnProperty
2 |
3 | function is(x: any, y: any) {
4 | if (x === y) {
5 | return x !== 0 || y !== 0 || 1 / x === 1 / y
6 | } else {
7 | return x !== x && y !== y
8 | }
9 | }
10 |
11 | export type AreEqual = (a: Value, b: Value) => boolean;
12 |
13 | export function shallowEqual(objA: any, objB: any) {
14 | if (is(objA, objB)) return true
15 |
16 | if (
17 | typeof objA !== 'object' ||
18 | objA === null ||
19 | typeof objB !== 'object' ||
20 | objB === null
21 | ) {
22 | return false
23 | }
24 |
25 | const keysA = Object.keys(objA)
26 | const keysB = Object.keys(objB)
27 |
28 | if (keysA.length !== keysB.length) return false
29 |
30 | for (let i = 0; i < keysA.length; i++) {
31 | if (!hasOwn.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
32 | return false
33 | }
34 | }
35 |
36 | return true
37 | }
38 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "declaration": true,
5 | "outDir": "dist",
6 | "strict": true,
7 | "allowSyntheticDefaultImports": true,
8 | "sourceMap": true,
9 | "jsx": "react",
10 | "lib": ["dom", "es5", "es2015.promise"]
11 | },
12 | "include": ["src/**/*"],
13 | "exclude": ["node_modules"]
14 | }
15 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | This website was created with [Docusaurus](https://docusaurus.io/).
2 |
3 | # What's In This Document
4 |
5 | * [Get Started in 5 Minutes](#get-started-in-5-minutes)
6 | * [Directory Structure](#directory-structure)
7 | * [Editing Content](#editing-content)
8 | * [Adding Content](#adding-content)
9 | * [Full Documentation](#full-documentation)
10 |
11 | # Get Started in 5 Minutes
12 |
13 | 1. Make sure all the dependencies for the website are installed:
14 |
15 | ```sh
16 | # Install dependencies
17 | $ yarn
18 | ```
19 | 2. Run your dev server:
20 |
21 | ```sh
22 | # Start the site
23 | $ yarn start
24 | ```
25 |
26 | ## Directory Structure
27 |
28 | Your project file structure should look something like this
29 |
30 | ```
31 | my-docusaurus/
32 | docs/
33 | doc-1.md
34 | doc-2.md
35 | doc-3.md
36 | website/
37 | blog/
38 | 2016-3-11-oldest-post.md
39 | 2017-10-24-newest-post.md
40 | core/
41 | node_modules/
42 | pages/
43 | static/
44 | css/
45 | img/
46 | package.json
47 | sidebar.json
48 | siteConfig.js
49 | ```
50 |
51 | # Editing Content
52 |
53 | ## Editing an existing docs page
54 |
55 | Edit docs by navigating to `docs/` and editing the corresponding document:
56 |
57 | `docs/doc-to-be-edited.md`
58 |
59 | ```markdown
60 | ---
61 | id: page-needs-edit
62 | title: This Doc Needs To Be Edited
63 | ---
64 |
65 | Edit me...
66 | ```
67 |
68 | For more information about docs, click [here](https://docusaurus.io/docs/en/navigation)
69 |
70 | ## Editing an existing blog post
71 |
72 | Edit blog posts by navigating to `website/blog` and editing the corresponding post:
73 |
74 | `website/blog/post-to-be-edited.md`
75 | ```markdown
76 | ---
77 | id: post-needs-edit
78 | title: This Blog Post Needs To Be Edited
79 | ---
80 |
81 | Edit me...
82 | ```
83 |
84 | For more information about blog posts, click [here](https://docusaurus.io/docs/en/adding-blog)
85 |
86 | # Adding Content
87 |
88 | ## Adding a new docs page to an existing sidebar
89 |
90 | 1. Create the doc as a new markdown file in `/docs`, example `docs/newly-created-doc.md`:
91 |
92 | ```md
93 | ---
94 | id: newly-created-doc
95 | title: This Doc Needs To Be Edited
96 | ---
97 |
98 | My new content here..
99 | ```
100 |
101 | 1. Refer to that doc's ID in an existing sidebar in `website/sidebar.json`:
102 |
103 | ```javascript
104 | // Add newly-created-doc to the Getting Started category of docs
105 | {
106 | "docs": {
107 | "Getting Started": [
108 | "quick-start",
109 | "newly-created-doc" // new doc here
110 | ],
111 | ...
112 | },
113 | ...
114 | }
115 | ```
116 |
117 | For more information about adding new docs, click [here](https://docusaurus.io/docs/en/navigation)
118 |
119 | ## Adding a new blog post
120 |
121 | 1. Make sure there is a header link to your blog in `website/siteConfig.js`:
122 |
123 | `website/siteConfig.js`
124 | ```javascript
125 | headerLinks: [
126 | ...
127 | { blog: true, label: 'Blog' },
128 | ...
129 | ]
130 | ```
131 |
132 | 2. Create the blog post with the format `YYYY-MM-DD-My-Blog-Post-Title.md` in `website/blog`:
133 |
134 | `website/blog/2018-05-21-New-Blog-Post.md`
135 |
136 | ```markdown
137 | ---
138 | author: Frank Li
139 | authorURL: https://twitter.com/foobarbaz
140 | authorFBID: 503283835
141 | title: New Blog Post
142 | ---
143 |
144 | Lorem Ipsum...
145 | ```
146 |
147 | For more information about blog posts, click [here](https://docusaurus.io/docs/en/adding-blog)
148 |
149 | ## Adding items to your site's top navigation bar
150 |
151 | 1. Add links to docs, custom pages or external links by editing the headerLinks field of `website/siteConfig.js`:
152 |
153 | `website/siteConfig.js`
154 | ```javascript
155 | {
156 | headerLinks: [
157 | ...
158 | /* you can add docs */
159 | { doc: 'my-examples', label: 'Examples' },
160 | /* you can add custom pages */
161 | { page: 'help', label: 'Help' },
162 | /* you can add external links */
163 | { href: 'https://github.com/facebook/Docusaurus', label: 'GitHub' },
164 | ...
165 | ],
166 | ...
167 | }
168 | ```
169 |
170 | For more information about the navigation bar, click [here](https://docusaurus.io/docs/en/navigation)
171 |
172 | ## Adding custom pages
173 |
174 | 1. Docusaurus uses React components to build pages. The components are saved as .js files in `website/pages/en`:
175 | 1. If you want your page to show up in your navigation header, you will need to update `website/siteConfig.js` to add to the `headerLinks` element:
176 |
177 | `website/siteConfig.js`
178 | ```javascript
179 | {
180 | headerLinks: [
181 | ...
182 | { page: 'my-new-custom-page', label: 'My New Custom Page' },
183 | ...
184 | ],
185 | ...
186 | }
187 | ```
188 |
189 | For more information about custom pages, click [here](https://docusaurus.io/docs/en/custom-pages).
190 |
191 | # Full Documentation
192 |
193 | Full documentation can be found on the [website](https://docusaurus.io/).
194 |
--------------------------------------------------------------------------------
/website/blog/2019-04-05-docs-website.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Docs Website First Attempt
3 | author: Adam Klein
4 | ---
5 |
6 | Docs website is up
7 |
8 |
9 |
10 | We decided to use Docusaurus and Github Pages to host our documentation.
11 | Our examples will use parcel for easy setup, and we will embed them using codesandbox inside the docs.
12 |
--------------------------------------------------------------------------------
/website/blog/2019-22-05-refactor-to-regular-hooks.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Complete Rewrite to Use React Hooks
3 | author: Adam Klein
4 | ---
5 |
6 | We've managed to find a way to use regular React hooks, which can leverage hooks libraries, project's custom hooks, ESLint rules and devtools.
7 |
8 |
9 |
10 | There were a few problems which this approach, the major one was that we couldn't define the store inside context because we had to setup the units prematurely, instead of lazily on the first use.
11 | This is because we need the unit's initial value synchronously the first time we invoke the hook.
12 |
--------------------------------------------------------------------------------
/website/core/Footer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | const React = require('react');
9 |
10 | class Footer extends React.Component {
11 | docUrl(doc, language) {
12 | const baseUrl = this.props.config.baseUrl;
13 | const docsUrl = this.props.config.docsUrl;
14 | const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`;
15 | const langPart = `${language ? `${language}/` : ''}`;
16 | return `${baseUrl}${docsPart}${langPart}${doc}`;
17 | }
18 |
19 | pageUrl(doc, language) {
20 | const baseUrl = this.props.config.baseUrl;
21 | return baseUrl + (language ? `${language}/` : '') + doc;
22 | }
23 |
24 | render() {
25 | return (
26 |
70 | );
71 | }
72 | }
73 |
74 | module.exports = Footer;
75 |
--------------------------------------------------------------------------------
/website/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "_comment": "This file is auto-generated by write-translations.js",
3 | "localized-strings": {
4 | "next": "Next",
5 | "previous": "Previous",
6 | "tagline": "A library for reusing state and business logic between components",
7 | "docs": {
8 | "basic-usage": {
9 | "title": "Getting Started with Reusable",
10 | "sidebar_label": "Getting Started"
11 | }
12 | },
13 | "links": {},
14 | "categories": {
15 | "Docs": "Docs"
16 | }
17 | },
18 | "pages-strings": {
19 | "Help Translate|recruit community translators for your project": "Help Translate",
20 | "Edit this Doc|recruitment message asking to edit the doc source": "Edit",
21 | "Translate this Doc|recruitment message asking to translate the docs": "Translate"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "examples": "docusaurus-examples",
4 | "start": "docusaurus-start",
5 | "build": "docusaurus-build",
6 | "publish-gh-pages": "docusaurus-publish",
7 | "write-translations": "docusaurus-write-translations",
8 | "version": "docusaurus-version",
9 | "rename-version": "docusaurus-rename-version"
10 | },
11 | "devDependencies": {
12 | "docusaurus": "^1.8.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/website/pages/en/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | const React = require('react');
9 |
10 | const CompLibrary = require('../../core/CompLibrary.js');
11 |
12 | const Container = CompLibrary.Container;
13 | const GridBlock = CompLibrary.GridBlock;
14 |
15 | class HomeSplash extends React.Component {
16 | render() {
17 | const { siteConfig, language = '' } = this.props;
18 | const { baseUrl, docsUrl } = siteConfig;
19 | const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`;
20 | const langPart = `${language ? `${language}/` : ''}`;
21 | const docUrl = doc => `${baseUrl}${docsPart}${langPart}${doc}`;
22 |
23 | const SplashContainer = props => (
24 |
25 |
26 |
{props.children}
27 |
28 |
29 | );
30 |
31 | const Logo = props => (
32 |
33 | );
34 |
35 | const ProjectTitle = () => (
36 |
37 |
38 | {siteConfig.title}
39 | {siteConfig.tagline}
40 |
41 | );
42 |
43 | const PromoSection = props => (
44 |
45 |
46 |
{props.children}
47 |
48 |
49 | );
50 |
51 | const Button = props => (
52 |
57 | );
58 |
59 | return (
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 | }
71 |
72 | class Index extends React.Component {
73 | render() {
74 | const { config: siteConfig, language = '' } = this.props;
75 | const { baseUrl } = siteConfig;
76 |
77 | const Block = props => (
78 |
82 |
87 |
88 | );
89 |
90 | const Showcase = () => {
91 | if ((siteConfig.users || []).length === 0) {
92 | return null;
93 | }
94 |
95 | const showcase = siteConfig.users
96 | .filter(user => user.pinned)
97 | .map(user => (
98 |
99 |
100 |
101 | ));
102 |
103 | const pageUrl = page => baseUrl + (language ? `${language}/` : '') + page;
104 |
105 | return (
106 |
107 |
Who is Using This?
108 |
This project is used by all these people
109 |
{showcase}
110 |
115 |
116 | );
117 | };
118 |
119 | return (
120 |
121 |
122 | {/*
123 |
124 |
*/}
125 |
126 | );
127 | }
128 | }
129 |
130 | module.exports = Index;
131 |
--------------------------------------------------------------------------------
/website/pages/en/users.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | const React = require('react');
9 |
10 | const CompLibrary = require('../../core/CompLibrary.js');
11 |
12 | const Container = CompLibrary.Container;
13 |
14 | class Users extends React.Component {
15 | render() {
16 | const {config: siteConfig} = this.props;
17 | if ((siteConfig.users || []).length === 0) {
18 | return null;
19 | }
20 |
21 | const editUrl = `${siteConfig.repoUrl}/edit/master/website/siteConfig.js`;
22 | const showcase = siteConfig.users.map(user => (
23 |
24 |
25 |
26 | ));
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
Who is Using This?
34 |
This project is used by many folks
35 |
36 |
{showcase}
37 |
Are you using this project?
38 |
39 | Add your company
40 |
41 |
42 |
43 |
44 | );
45 | }
46 | }
47 |
48 | module.exports = Users;
49 |
--------------------------------------------------------------------------------
/website/sidebars.json:
--------------------------------------------------------------------------------
1 | {
2 | "docs": {
3 | "Docs": [
4 | "basic-usage"
5 | ]
6 | }
7 | }
--------------------------------------------------------------------------------
/website/siteConfig.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) 2017-present, Facebook, Inc.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | // See https://docusaurus.io/docs/site-config for all the possible
9 | // site configuration options.
10 |
11 | // List of projects/orgs using your project for the users page.
12 | const users = [
13 | {
14 | caption: 'Mimic',
15 | // You will need to prepend the image path with your baseUrl
16 | // if it is not '/', like: '/test-site/img/docusaurus.svg'.
17 | infoLink: 'https://mimic.js.org',
18 | pinned: true,
19 | },
20 | ];
21 |
22 | const siteConfig = {
23 | title: 'ReusableJS', // Title for your website.
24 | tagline: 'A library for reusing state and business logic between components',
25 | url: 'https://reusable.github.io', // Your website URL
26 | baseUrl: '/reusable/', // Base URL for your project */
27 | // For github.io type URLs, you would set the url and baseUrl like:
28 | // url: 'https://facebook.github.io',
29 | // baseUrl: '/test-site/',
30 |
31 | // Used for publishing and more
32 | projectName: 'reusable',
33 | organizationName: 'reusablejs',
34 | // For top-level user or org sites, the organization is still the same.
35 | // e.g., for the https://JoelMarcey.github.io site, it would be set like...
36 | // organizationName: 'JoelMarcey'
37 |
38 | // For no header links in the top nav bar -> headerLinks: [],
39 | headerLinks: [
40 | // { doc: 'basic-usage', label: 'Getting Started' }
41 | ],
42 |
43 | // If you have users set above, you add it here:
44 | users,
45 |
46 | /* path to images for header/footer */
47 | headerIcon: 'img/reusable.png',
48 | footerIcon: 'img/reusable.png',
49 | favicon: 'img/reusable.png',
50 |
51 | /* Colors for website */
52 | colors: {
53 | primaryColor: '#385170',
54 | secondaryColor: '#205C3B',
55 | },
56 |
57 | /* Custom fonts for website */
58 | /*
59 | fonts: {
60 | myFont: [
61 | "Times New Roman",
62 | "Serif"
63 | ],
64 | myOtherFont: [
65 | "-apple-system",
66 | "system-ui"
67 | ]
68 | },
69 | */
70 |
71 | // This copyright info is used in /core/Footer.js and blog RSS/Atom feeds.
72 | copyright: `Copyright © ${new Date().getFullYear()} ReusablesJS`,
73 |
74 | highlight: {
75 | // Highlight.js theme to use for syntax highlighting in code blocks.
76 | theme: 'default',
77 | },
78 |
79 | // Add custom scripts here that would be placed in