Skip to content

Commit

Permalink
feat: add default store to simplify react & vue hook
Browse files Browse the repository at this point in the history
  • Loading branch information
e7h4n committed Dec 22, 2024
1 parent a977038 commit fbd5f08
Show file tree
Hide file tree
Showing 10 changed files with 47 additions and 39 deletions.
7 changes: 7 additions & 0 deletions .changeset/perfect-numbers-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'ccstate': minor
'ccstate-react': minor
'ccstate-vue': minor
---

feat: add default store
14 changes: 1 addition & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,27 +85,15 @@ export default function App() {
</div>
);
}
```
Use `createStore` and `StoreProvider` to provide a CCState store to React, all states and computations will only affect this isolated store.

```tsx
// main.jsx
import { createStore } from 'ccstate';
import { StoreProvider } from 'ccstate-react';
import { createRoot } from 'react-dom/client';

import App from './App';

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);

const store = createStore();
root.render(
<StoreProvider value={store}>
<App />
</StoreProvider>,
);
root.render(<App />);
```
That's it! [Click here to see the full example](https://codesandbox.io/p/sandbox/cr3xg6).
Expand Down
8 changes: 7 additions & 1 deletion packages/ccstate/src/core/__tests__/store.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import { computed, createStore, command, state } from '..';
import { computed, createStore, command, state, getDefaultStore } from '..';
import type { Getter } from '..';
import { suspense } from './utils';
import { createDebugStore } from '../../debug';
Expand Down Expand Up @@ -761,3 +761,9 @@ it('shoule unmount base$ atom in this complex scenario 4', () => {

expect(store.isMounted(base$)).toBeTruthy();
});

it('get default store should be same', () => {
const store1 = getDefaultStore();
const store2 = getDefaultStore();
expect(store1).toBe(store2);
});
2 changes: 1 addition & 1 deletion packages/ccstate/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { state, computed, command } from './atom';
export { createStore } from './store';
export { createStore, getDefaultStore } from './store';

export type { State, Computed, Command, Getter, Setter, Updater, Read, Write } from '../../types/core/atom';

Expand Down
8 changes: 8 additions & 0 deletions packages/ccstate/src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,11 @@ export function createStore(): Store {

return new StoreImpl(atomManager, listenerManager);
}

let defaultStore: Store | undefined = undefined;
export function getDefaultStore(): Store {
if (!defaultStore) {
defaultStore = createStore();
}
return defaultStore;
}
2 changes: 1 addition & 1 deletion packages/ccstate/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { state, computed, command, createStore } from './core';
export { state, computed, command, createStore, getDefaultStore } from './core';

export type { State, Computed, Command, Getter, Setter, Updater, Subscribe, Store, Read, Write } from './core';

Expand Down
22 changes: 9 additions & 13 deletions packages/react/src/__tests__/get-and-set.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { render, cleanup, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { computed, createStore, command, state, createDebugStore } from 'ccstate';
import { computed, createStore, command, state, createDebugStore, getDefaultStore } from 'ccstate';
import { StoreProvider, useGet, useSet } from '..';
import { StrictMode, useState } from 'react';
import '@testing-library/jest-dom/vitest';
Expand Down Expand Up @@ -199,25 +199,21 @@ describe('react', () => {
expect(await screen.findByText('1')).toBeInTheDocument();
});

it('will throw error if no provider', () => {
it('should use default store if no provider', () => {
const count$ = state(0);
getDefaultStore().set(count$, 10);

function App() {
const count = useGet(count$);
return <div>{count}</div>;
}

// suppress react render error message in console
const mock = vi.spyOn(console, 'error').mockImplementation(() => void 0);
expect(() =>
render(
<StrictMode>
<App />
</StrictMode>,
),
).toThrow();

mock.mockRestore();
render(
<StrictMode>
<App />
</StrictMode>,
);
expect(screen.getByText('10')).toBeInTheDocument();
});

it('will unmount when component cleanup', async () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createContext, useContext } from 'react';
import { getDefaultStore } from 'ccstate';
import type { Store } from 'ccstate';

const StoreContext = createContext<Store | null>(null);
Expand All @@ -9,7 +10,7 @@ export function useStore(): Store {
const store = useContext(StoreContext);

if (!store) {
throw new Error('Store context not found - did you forget to wrap your app with StoreProvider?');
return getDefaultStore();
}

return store;
Expand Down
17 changes: 9 additions & 8 deletions packages/vue/src/__tests__/get-set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import '@testing-library/jest-dom/vitest';
import { fireEvent, render, cleanup, screen } from '@testing-library/vue';
import { afterEach, expect, it } from 'vitest';
import { command, createStore, state } from 'ccstate';
import { command, createStore, getDefaultStore, state } from 'ccstate';
import { provideStore } from '../provider';
import { useGet, useSet } from '..';

Expand Down Expand Up @@ -87,8 +87,9 @@ it('call command by useSet', async () => {
expect(screen.getByText('Times clicked: 30')).toBeInTheDocument();
});

it('throw if can not find store', () => {
it('should use default store if no provider', () => {
const count$ = state(0);
getDefaultStore().set(count$, 10);

const Component = {
setup() {
Expand All @@ -102,10 +103,10 @@ it('throw if can not find store', () => {
`,
};

expect(() => {
render({
components: { Component },
template: `<div><Component /></div>`,
});
}).toThrow();
render({
components: { Component },
template: `<div><Component /></div>`,
});

expect(screen.getByText('10')).toBeInTheDocument();
});
3 changes: 2 additions & 1 deletion packages/vue/src/provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { inject, provide, type InjectionKey } from 'vue';
import { getDefaultStore } from 'ccstate';
import type { Store } from 'ccstate';

export const StoreKey = Symbol('ccstate-vue-store') as InjectionKey<Store>;
Expand All @@ -10,7 +11,7 @@ export const provideStore = (store: Store) => {
export const useStore = (): Store => {
const store = inject(StoreKey);
if (store === undefined) {
throw new Error('Store context not found - did you forget to wrap your app with StoreProvider?');
return getDefaultStore();
}

return store;
Expand Down

0 comments on commit fbd5f08

Please sign in to comment.