Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/stores #44

Merged
merged 9 commits into from
Sep 11, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 19 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 12 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cobuildlab/react-simple-state",
"version": "0.6.2",
"version": "0.7.0",
"description": "Simple and Lightweight state management for react applications. ",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
Expand Down Expand Up @@ -39,15 +39,23 @@
"jest": "^26.0.1",
"lint-staged": "^10.2.2",
"prettier": "^2.0.5",
"react": "^16.13.1",
"react": "17.0.2",
"react-test-renderer": "^16.13.1",
"ts-jest": "^25.5.1",
"typedoc": "^0.17.8",
"typedoc-plugin-markdown": "^2.3.1",
"typescript": "^4.2.2"
"typescript": "^4.4.2"
},
"peerDependencies": {
"react": "^16.8.6"
"react": ">=16.13.1"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
},
"husky": {
"hooks": {
Expand Down
6 changes: 4 additions & 2 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ export function createAction<T, U extends any[], E = Error, R = unknown>(
try {
data = await action(...params);
} catch (error) {
errorEvent.dispatch(error);
return { error };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errorEvent.dispatch(error as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return { error } as { error: any };
}

event.dispatch(data);
Expand Down
60 changes: 59 additions & 1 deletion src/pub-sub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@ export interface Subscriber<T> {
update: (value: T | null) => void;
}

export interface SubscriberV2<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use Subscriber<T> and we can remove the | null typing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alacret Done

I think we should keep a similar API to the events with a subscribe method. And rather the store needs to provide 2 functions, one for the state, one for the error.

useSubscription(Event, (state) => {})
useSubscription(Store, (state) => {}, (error) => {})

I've crea two hooks useStoreSubcription and useStoreErrorSubscription that handle both scenarios separately

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to manage both scenarios separately then what's the benefit of using a store instead of 2 events? or what's the difference?

My guess is that we want to reduce the boilerplate. That's why I'm suggesting a single hook. Maybe both functions can be optional

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having both is better for out internal composability and performance.

for example the useStore hook use useStoreSubcription internally if we only have one useSubscription the useStore hook would be inheriting the logic for the errors but it won't need it.

So yes, it have more boilerplate but it is only internally the hooks that we will do not have it

Also i want you to keep in mind that the way i approaching this implementation is step by step, because we don't want to introduce breaking changes and have a lot of work to keep the leagacy features neither

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jesusrodrz we need at least a second opinion here.

You say: Having both is better for our internal composability and performance.

But this is what we already have with the Events API so having it separately not sure what's the benefit of that over the current Events API

Also, we have to keep in mind always:

We have to aim for a unique API, we don't want to have 2 ways of doing the same thing. So this Store API has to be thought to replace the Events API if and only if we can make it better.

Copy link
Contributor

@alacret alacret Sep 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing the hook separately, 99% or our use cases looks like this:

  • Events API
// events
const Event = createEvent<Onboarding>({
  initialValue: new Onboarding():
 reducer: (onboarding) => onboarding
});
const ErrorEvent = createEvent<Error | null>();

// action
const actionWithCreatAction = createAction (Event, EventError, async (onaboarding) =>{
  const client = apolloClient.get();
  let result = await client.mutate(UPDATE_ONBOARDING, {onboarding});
  return new Onboarding(result)
});

export default function App() {
  const onboarding = useEvent(Event);

  useSubscription(Event, (onboarding) => {
      //
  });

  useSubscription(ErrorEvent, (error:Error) => {
    //
  });

  return (
    <div className="App">
      <h1>{{onboarding}}</h1>
      <span>{{onboarding.getUserEmail()}}</span>
      <Button onClick={} text="Save" />
    </div>
  );
}
  • Store API
// store
const store = createStore<Onboarding>({
  initialValue: new Onboarding():
 reducer: (onboarding) => onboarding
});

// action
const actionWithCreatAction = createActionFromStore (store, async (onaboarding) =>{
  const client = apolloClient.get();
  let result = await client.mutate(UPDATE_ONBOARDING, {onboarding});
  return new Onboarding(result)
});

export default function App() {
  const onboarding = useStoreValue(Event);

  useStoreSubscription(store, (onboarding) => {
      //
  });

  useStoreErrorSubscription(store, (error:Error) => {
    //
  });

  return (
    <div className="App">
      <h1>{{onboarding}}</h1>
      <span>{{onboarding.getUserEmail()}}</span>
      <Button onClick={} text="Save" />
    </div>
  );
}

Which looks pretty much the same

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but this the first interations, with the next features like the other hooks i will be more usefull and simpler.
But i don't wanna to have a huge pull request, let move on step by step.

And that the pattern that i want to avoid, it is very imperative. the idea is that we dont have to use the useSubscription in most of the cases.

An replaced with a new hooks api that we only hace to declare one store, and s cople of actions

update: (value: T) => void;
}
export interface Publisher<T> {
subscribers: Subscriber<T>[];

subscribe(subscriber: Subscriber<T>): Subscription;

notify(value: T | null): void;
}
export interface PublisherV2<T> {
alacret marked this conversation as resolved.
Show resolved Hide resolved
subscribers: SubscriberV2<T>[];

subscribe(subscriber: SubscriberV2<T>): Subscription;

notify(value: T): void;
}

/**
* An Object that represent the subscription to a Publisher.
Expand All @@ -38,6 +48,27 @@ class ConcreteSubscription<T> implements Subscription {
}
}

class ConcreteSubscriptionV2<T> implements Subscription {
alacret marked this conversation as resolved.
Show resolved Hide resolved
private readonly publisher: PublisherV2<T> | null = null;
private readonly subscriber: SubscriberV2<T> | null = null;

constructor(publisher: PublisherV2<T>, subscriber: SubscriberV2<T>) {
this.publisher = publisher;
this.subscriber = subscriber;
}

unsubscribe(): void {
if (this.publisher === null || this.subscriber === null)
throw new Error('ConcreteSubscription: Invalid state');

const observerIndex = this.publisher.subscribers.indexOf(this.subscriber);
if (observerIndex === -1) {
return console.log('ConcreteSubscription: Nonexistent subscriber.');
}
this.publisher.subscribers.splice(observerIndex, 1);
}
}

/**
* A simple publisher.
*/
Expand All @@ -60,4 +91,31 @@ class ConcretePublisher<T> implements Publisher<T> {
}
}

export { ConcretePublisher, ConcreteSubscription };
/**
* A simple publisher.
*/
class ConcretePublisherV2<T> implements PublisherV2<T> {
public subscribers: SubscriberV2<T>[] = [];

public subscribe(subscriber: SubscriberV2<T>): Subscription {
const isExist = this.subscribers.includes(subscriber);
if (isExist) {
throw new Error('Publisher: Subscriber has been subscribed already.');
}
this.subscribers.push(subscriber);
return new ConcreteSubscriptionV2(this, subscriber);
}

public notify(value: T): void {
for (const subscriber of this.subscribers) {
subscriber.update(value);
}
}
}

export {
ConcretePublisher,
ConcreteSubscription,
ConcreteSubscriptionV2,
ConcretePublisherV2,
};
56 changes: 56 additions & 0 deletions src/store-hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect, useRef, useState } from 'react';
import { Store } from './store';

/**
* @param {Store} store - Store to subscribe.
* @param {Function} callback - Function to call on each dipatch.
* @param {Function} errorcCallback - Function to call on each error dipatch.
*/
export function useStoreSub<T>(
alacret marked this conversation as resolved.
Show resolved Hide resolved
store: Store<T>,
callback: ((data: T) => void) | undefined = undefined,
errorcCallback: ((data: Error) => void) | undefined = undefined,
): void {
const callbacksRef = useRef({
callback,
errorcCallback,
});

callbacksRef.current = {
callback,
errorcCallback,
};

useEffect(() => {
const unsubscribeSuccess = store.subscribe((data) => {
if (callbacksRef.current.callback) {
callbacksRef.current.callback(data);
}
});

const unsubscribeError = store.subscribeError((data) => {
if (callbacksRef.current.errorcCallback) {
callbacksRef.current.errorcCallback(data);
}
});

return () => {
unsubscribeSuccess.unsubscribe();
unsubscribeError.unsubscribe();
};
}, [store]);
}

/**
* @param {Store} store - Store to subscribe.
* @returns {Object} - Resulto object from the store.
*/
export function useStore<T>(store: Store<T>): T {
const [state, setState] = useState(store.get());

useStoreSub(store, (data) => {
setState(data);
});

return state;
}
34 changes: 34 additions & 0 deletions src/store-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { CheckDispatchType, Store } from './store';

/**
* @param {Store} store - Event.
* @param {Function} callback - Callback.
* @param {Function} sideEffect - Callback.
* @returns {Function} Reducer fucntion.
*/
export function createStoreAction<T, V extends unknown[], U = unknown>(
store: Store<T, U>,
callback:
| ((prevState: T, ...params: V) => CheckDispatchType<T, U>)
| ((prevState: T, ...params: V) => Promise<CheckDispatchType<T, U>>),
sideEffect?: (...params: V) => void,
) {
return (...params: V): void => {
if (sideEffect) {
sideEffect(...params);
}
const result = callback(store.get(), ...params);

if (result instanceof Promise) {
result
.then((data) => {
store.dispatch(data);
})
.catch((e) => {
store.dispatchError(e);
});
return;
}
store.dispatch(result);
};
}
Loading