4.0.5 • Published 2 months ago

@jacobtipp/react-bloc v4.0.5

Weekly downloads
-
License
MIT
Repository
-
Last release
2 months ago

@jacobtipp/react-bloc

Introduction

React components and hooks for bloc-state

Installation

npm install @jacobtipp/bloc @jacobtipp/react-bloc rxjs

Components

RootProvider

A root provider component that sets up the context for accessing Provider instances. This is required at the root of your application.

Example

ReactDOM.render(
  <RootProvider>
    <App >
  </RootProvider>,
  document.getElementById('root'),
);

Provider

A Provider component that manages the lifecycle of a provided class instance.

Props

export interface ProviderProps<Class extends AnyClassType> {
  /** The class definition to be used. */
  classDef: Class;
  /** Function to create an instance or an instance of the class itself. */
  create: (() => InstanceType<Class>) | InstanceType<Class>;
  /** Optional callback executed when the component mounts. */
  onMount?: (instance: InstanceType<Class>) => void;
  /** Optional callback executed when the component unmounts. */
  onUnmount?: (instance: InstanceType<Class>) => void;
  /** Time in milliseconds to wait before disposing of a non-mounted instance. */
  disposeTime?: number;
  /** Child nodes to be rendered. */
  children: ReactNode;
  /** Dependencies to trigger re-creation of the instance. */
  dependencies?: any[];
}

Example

class UserRepository {
  getUser() {
    //...code 
  }
} 

const UserRepositoryProvider = ({children}: PropsWithChildren) => {
  return (
    <Provider
      classDef={UserRepository}
      create={() => new UserRepository()}
    >
      {children}
    </Provider>
  )
}

MultiProvider

A component that composes multiple provider components together. The provider array order matters, they should be in order of dependency.

Example

const Providers = () => {
  // Dependency order UserApiClient -> UserRepository -> UserBloc
  return (
    <MultiProvider providers={[
      UserApiClientProvider, 
      UserRepositoryProvider, 
      UserBlocProvider 
    ]}>
  )
}

BlocProvider

A component that provides a Bloc instance to its children, utilizing the generic Provider component

⚠️ If a function is used to create a Bloc instance, the BlocProvider will close the Bloc instance when it is unmounted. Manually provided instances are never closed by the BlocProvider

Props

export interface BlocProviderProps<Bloc extends ClassType<BlocBase<any>>> {
  /** The Bloc class to be provided. */
  bloc: Bloc;
  /** Function to create an instance of the Bloc or the instance itself. */
  create: (() => InstanceType<Bloc>) | InstanceType<Bloc>;
  /** Optional callback to be executed when the Bloc is mounted. */
  onMount?: (bloc: InstanceType<Bloc>) => void;
  /** ReactNode children to be rendered within the provider. */
  children: ReactNode;
  /** Optional dependencies array to trigger re-creation of the Bloc instance. */
  dependencies?: any[];
}

Example

class UserBloc extends Cubit<UserState> {
  constructor(private readonly userRepo: UserRepository)

  getUser = async () => {
    try {
      const user = await this.userRepository.getUser()
      this.emit(user)
    } catch (e) {
      assertIsError(e)
      this.addError(e)
    }
  } 
}

const UserBlocProvider = ({children}: PropsWithChildren) => {
  // grab the UserRepository instance that was provided higher up in the tree
  const userRepository = useProvider(UserRepository)

  return (
    <BlocProvider
      bloc={UserBloc}
      create={() => new UserBloc(userRepository)}  
      onMount={(userBloc) => userBloc.getUser()}
    >
      {children}
    </BlocProvider>
  )
}

BlocBuilder

A component that listens to state changes from a Bloc and rebuilds its UI accordingly.

This component uses a provided bloc to listen for state changes and uses the builder function to rebuild the UI based on the current state. An optional buildWhen function can be provided to determine if the UI should rebuild on a state change.

Props

export interface BlocBuilderProps<
  Bloc extends ClassType<BlocBase<any>>,
  State = StateType<InstanceType<Bloc>>
> {
  /** The Bloc class to observe for state changes. */
  bloc: Bloc;
  /** Function to build the UI based on the current state. */
  builder: (state: State) => JSX.Element;
  /** Optional function to determine if the UI should be rebuilt when the state changes. */
  buildWhen?: (previous: State, current: State) => boolean;
}

Example

export const UserDetails = () => {
  const userBloc = useBlocInstance(UserBloc);

  return (
    <BlocBuilder
      bloc={UserBloc}
      buildWhen={(previous, current) => previous !== current}
      builder={(state) => {
        if (state.isLoading) {
          return <div>loading...</div>
        }
        return (
          <>
            <p>{state.name}</p>
            <p>{state.info}</p>
          </>
        );
      }}
    />
  );
};

BlocListener

A component that uses the useBlocListener hook to perform side effects in response to state changes in a specified Bloc. It does not render any additional UI elements itself but provides a mechanism to respond to Bloc state changes while rendering its child components.

⚠️ alternatively you can use a useBlocListener hook instead of the BlocListener component see useBlocListener

const UserPage = () => {
  const snackbar = useSnackbar()

  return (
    <BlocListener
      bloc={UserBloc}
      listenWhen={(previous, current) => previous !== current && current.hasError}
      listener={(bloc, state) => snackbar.open(state.error?.message) }
    >
      <SomeChildComponent />
    </BlocListener>
  )
}

// if you need multiple bloc listeners, create siblings

const UserPage = () => {
  const snackbar = useSnackbar()
  const router = useRouter()

  return (
    <>
      <BlocListener
        bloc={UserBloc}
        listenWhen={(previous, current) => previous !== current && current.hasError}
        listener={(bloc, state) => snackbar.open(state.error?.message) }
      />
      <BlocListener
        bloc={UserBloc}
        listenWhen={(previous, current) => previous !== current && !current.data.isAuthenticated}
        listener={(bloc, state) => router.push("/login")}
      />
      <SomeChildComponent />
    </>
  )
}

BlocConsumer

A component that combines the functionalities of both BlocBuilder and BlocListener. It listens to state changes and events from a specified Bloc and rebuilds its children based on the current state, while also allowing for side-effects based on the same or different conditions.

Props

export type BlocConsumerProps<
 Bloc extends ClassType<BlocBase<any>>,
  State = StateType<InstanceType<Bloc>>
> {
  /** The Bloc class to observe for state changes and to listen for specific events. */
  bloc: Bloc;
  /** Function to be called when the specified conditions are met. */
  listener: (bloc: InstanceType<Bloc>, state: State) => void;
  /** Optional function to determine whether the listener should be called based on state changes. */
  listenWhen?: (previous: State, current: State) => boolean;
  /** Function to build the UI based on the current state. */
  builder: (state: State) => JSX.Element;
  /** Optional function to determine if the UI should be rebuilt when the state changes. */
  buildWhen?: (previous: State, current: State) => boolean;
}

Example

export const UserDetails = () => {
  const userBloc = useBlocInstance(UserBloc);
  const router = useRouter()

  return (
    <BlocConsumer
      bloc={UserBloc}
      listenWhen={(previous, current) => previous !== current && !current.isAuthenticated}
      listener={(userBloc, state) => router.push('/login') }
      buildWhen={(previous, current) => previous !== current}
      builder={(state) => {
        if (state.isLoading) {
          return <div>loading...</div>
        }
        return (
          <>
            <p>{state.name}</p>
            <p>{state.info}</p>
          </>
        );
      }}
    />
  );
};

BlocErrorBoundary

A component that wraps its children in an error boundary specific to a Bloc. It uses a provided Bloc instance to handle errors and define reset behavior, along with a fallback UI component to display when an error occurs.

⚠️BlocErrorBoundary can be triggered by supplying an errorWhen callback to a useBloc or useBlocSelector hook see Hooks

Props

export type BlocErrorBoundaryProps<Bloc extends ClassType<BlocBase<any>>> = {
  /** The Bloc class for which errors should be caught. */
  bloc: Bloc;
  /** Function to call when attempting to reset the error state, with the bloc instance as an argument. */
  onReset: (bloc: InstanceType<Bloc>) => void;
  /** A React component to render as a fallback UI when an error is caught. */
  fallback: React.ComponentType<FallbackProps>;
} & PropsWithChildren;

Example

export const UserErrorBoundary = ({ children}: PropsWithChildren) => (
  <BlocErrorBoundary
    bloc={UserBloc}
    fallback={UserBlocErrorFallback}
    onReset={(userBloc) =>
      userBloc.add(
        new ResetUserEvent())
      )
    }
  >
    {children}
  </BlocErrorBoundary>
);

Hooks

useProvider

A custom React hook that retrieves an instance of a class from a context map. This hook is designed to be used within a context provider system where class instances are stored in a context map by their class names. It facilitates the access to these instances by other components down the React component tree.

Example

const UserBlocProvider = ({children}: PropsWithChildren) => {
  const userRepository = useProvider(UserRepository)

  return (
    <BlocProvider
      bloc={UserBloc}
      create={() => new UserBloc(userRepository)}  
      onMount={(userBloc) => userBloc.getUser()}
    >
      {children}
    </BlocProvider>
  )
}

useBlocInstance

A custom hook that retrieves an instance of a specified Bloc from the nearest provider up the component tree. This hook abstracts the logic for accessing Bloc instances, making it easier to consume Blocs within React components.

useBlocInstance does not cause any rerenders

export const UserComponent = () => {
  const bloc = useBlocInstance(UserBloc); // returns the bloc instance from context

  return (
    <>
      <a onClick={() => bloc.add(new UserUpdateProfileEvent()))}></a>
    </>
  );
};

useBlocValue

A custom React hook that subscribes to the state of a specified Bloc and returns its current value. This hook uses useSyncExternalStore to subscribe to Bloc state changes in a way that is compatible with React's concurrent features, ensuring that the component using this hook re-renders with the latest state.

export const UserComponent = () => {
  const state = useBlocValue(UserBloc); // returns the current state value from a bloc instance

  return (
    <>
      <p>User: {state.name}</p>
    </>
  );
};

useBlocSelector

Custom hook to select and use a specific piece of state from a Bloc, with support for suspense and error handling.

Props

/**
 * Configuration options for `useBlocSelector` hook to customize behavior for selecting,
 * listening, suspending, and error handling based on the state of a Bloc.
 */
export type UseBlocSelectorConfig<Bloc extends BlocBase<any>, SelectedState> = {
  /**
   * A function that takes the current state of the Bloc and returns a transformed version of it.
   * This allows for selecting a specific part of the state or deriving new values from it.
   */
  selector?: (state: StateType<Bloc>) => SelectedState;

  /**
   * A function that determines whether the listener should be notified of the state change.
   * This can be used to optimize performance by avoiding unnecessary updates and re-renders.
   */
  listenWhen?: (state: StateType<Bloc>) => boolean;

  /**
   * A function that determines whether the component using this hook should trigger React suspense.
   * This is useful for delaying the rendering of the component until certain conditions are met.
   */
  suspendWhen?: (state: StateType<Bloc>) => boolean;

  /**
   * A function that determines whether an error should be thrown based on the current state of the Bloc.
   * This allows for error boundaries to catch and handle errors related to state conditions.
   */
  errorWhen?: (state: StateType<Bloc>) => boolean;
};

Example

type UserComponentProps = {
  id: number;
};

export const UserComponent = ({ id }: UserComponentProps) => {
  const lastName = useBlocSelector(UserBloc, {
    selector: (state) => state.name.last,
    listenWhen: (state) => state.id === id,
    suspendWhen: (state) => state.status === 'loading',
    errorWhen: (state) => state.error != null,
  });

  return (
    <>
      <p>{lastName}</p>
    </>
  );
};

useBloc

A custom React hook that combines the functionalities of useBlocInstance and useBlocSelector. It provides both the Bloc instance and the selected state, allowing components to easily interact with a Bloc's state and methods.

Example

export const UserComponent = () => {

  // returns a tuple with the state as first item and the bloc instance as second item
  // optionally takes a useBlocSelector config object, so it can be used to derive state as well as emit events with a bloc instance
  const [id, bloc] = useBloc(UserBloc, {
    selector: (state) => state.id,
  });

  return (
    <>
      <a onClick={() => bloc.add(new UserLoggedOutEvent(id))}></a>
    </>
  );
};

useBlocListener

A custom hook that listens to state changes in a Bloc and triggers a callback function based on those changes. It provides a mechanism to perform side effects in response to Bloc state updates.

Props

export interface BlocListenerProps<
  Bloc extends ClassType<BlocBase<any>>,
  State = StateType<InstanceType<Bloc>>
> {
  /** Function to be called when the specified conditions are met. */
  listener: (bloc: InstanceType<Bloc>, state: State) => void;
  /** Optional function to determine whether the listener should be called based on state changes. */
  listenWhen?: (previous: State, current: State) => boolean;
}

Example

export const UserComponent = () => {
  const router = useRouter()

  useBlocListener(UserBloc, {
    listenWhen(_previous, current) {
      return current.logoutSubmitSuccess;
    },
    listener() {
      router.push('/');
    },
  });
  
  const [id, bloc] = useBloc(UserBloc, {
    selector: (state) => state.id,
  });

  return (
    <>
      <a onClick={() => bloc.add(new UserLoggedOutEvent(id))}></a>
    </>
  );
};
4.0.5-next.3

2 months ago

4.0.5-next.2

2 months ago

4.0.5-next.1

2 months ago

4.0.5

2 months ago

4.0.4

2 months ago

4.0.3

3 months ago

4.0.2

3 months ago

4.0.1

3 months ago

4.0.0

3 months ago

4.0.0-next.4

3 months ago

4.0.0-next.3

3 months ago

4.0.0-next.2

3 months ago

4.0.0-next.1

3 months ago

3.6.0

4 months ago

3.4.0

4 months ago

3.3.1

4 months ago

3.5.0

4 months ago

3.3.0

4 months ago

3.2.5

5 months ago

3.2.4

5 months ago

3.2.3

5 months ago

3.2.2

5 months ago

2.0.3

7 months ago

2.0.2

7 months ago

2.0.5

7 months ago

2.0.4

7 months ago

2.0.7

6 months ago

2.0.6

7 months ago

2.0.1

7 months ago

2.0.0

7 months ago

3.2.1

5 months ago

3.2.0

5 months ago

3.0.2

5 months ago

3.0.1

5 months ago

3.0.0

6 months ago

1.1.1

7 months ago

1.1.0

7 months ago

3.1.0

5 months ago

2.0.0-dev.1

8 months ago

2.0.0-dev.3

8 months ago

2.0.0-dev.2

8 months ago

2.0.0-dev.5

8 months ago

2.0.0-dev.4

8 months ago

2.0.0-dev.7

8 months ago

2.0.0-dev.6

8 months ago

1.0.2

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago

1.0.7

1 year ago

1.0.6

1 year ago

1.0.5

1 year ago

1.0.4

1 year ago

1.0.3

1 year ago

1.0.0-beta.3

1 year ago

1.0.0-beta.4

1 year ago

1.0.0-beta.5

1 year ago

1.0.0-beta.10

1 year ago

1.0.0-beta.6

1 year ago

1.0.0-beta.7

1 year ago

1.0.0-beta.8

1 year ago

1.0.0-beta.9

1 year ago

1.0.0-beta.2

1 year ago

1.0.0-beta.1

1 year ago