Compare commits

...

2 commits

Author SHA1 Message Date
8c0c616f15
Add usePreviousValue hook. 2024-07-13 16:37:24 +02:00
6d3aafb15c
Add global state system. 2024-07-13 16:37:07 +02:00
2 changed files with 203 additions and 0 deletions

184
src/GlobalState.tsx Normal file
View file

@ -0,0 +1,184 @@
import React, {PropsWithChildren, useCallback, useContext, useState} from "react";
/**
* Global state modifiers functions.
*/
export class GlobalStateReducers<S>
{
/**
* The current state (readonly).
* @protected
*/
protected state: Readonly<S>;
/**
* State update system.
* @private
*/
private stateUpdater: React.Dispatch<S>;
/**
* Assign changes to the current state.
* @param stateUpdate Changes to apply to the current state.
*/
setState(stateUpdate: Partial<S>)
{
// Apply update object to the current state.
this.state = Object.assign({}, this.state, stateUpdate);
// Update the changed state.
this.stateUpdater(this.state);
}
}
/**
* Reducers definition interface for easy type definition.
*/
interface ReducerHack<S>
{
/**
* The current state (readonly).
*/
state: Readonly<S>;
/**
* State update system.
*/
stateUpdater: React.Dispatch<S>;
}
/**
* Main global state class.
*/
export class GlobalState<S, R extends GlobalStateReducers<S>>
{
/**
* Global state context.
* @protected
*/
protected context: React.Context<S>;
/**
* State update system, used in the reducers class.
* @protected
*/
protected stateUpdater: React.Dispatch<S>;
/**
* Create a new global state.
* @param defaultValue The default value of the global state.
* @param reducers Reducers class.
*/
constructor(protected defaultValue: S, protected reducers: R)
{
// Create a new context for the global state.
this.context = React.createContext<S>(defaultValue);
}
/**
* Get the context for the global state.
*/
getContext(): React.Context<S>
{ return this.context; }
/**
* Get the default value of the global state.
*/
getDefaultValue(): S
{ return this.defaultValue; }
/**
* Assign the state update system, used in the reducers class.
* @param updater State update system instance.
*/
setStateUpdater(updater: React.Dispatch<S>): void
{
this.stateUpdater = updater;
}
/**
* Get the configured state reducers class for the current state.
* @param state The state on which the reducers should apply.
*/
getStateReducers(state: S): R
{
// Assign the given state.
(this.reducers as unknown as ReducerHack<S>).state = state;
// Assign the current state updater.
(this.reducers as unknown as ReducerHack<S>).stateUpdater = this.stateUpdater;
return this.reducers;
}
}
/**
* Global state Provider hook.
* @param globalState Global state for which to get the global state Provider.
*/
export function useGlobalStateProvider<S, R extends GlobalStateReducers<S>>(globalState: GlobalState<S, R>): React.FunctionComponent<PropsWithChildren<{}>>
{
// Get the provider from the given global state context.
const Provider = globalState.getContext().Provider;
// Create the global state provider component.
return useCallback((props: PropsWithChildren<{}>) => {
// Get the current state of the global state.
const [state, setState] = useState(globalState.getDefaultValue());
// Pass the state update system to the global state.
globalState.setStateUpdater(setState);
// Use the context provider and render children.
return (
<Provider value={state}>
{props.children}
</Provider>
);
}, [Provider]);
}
/**
* Global state Provider component
* @constructor
*/
export function GlobalStateProvider<S, R extends GlobalStateReducers<S>>({globalState, children}: React.PropsWithChildren<{
/**
* Global state for which to get the global state Provider.
*/
globalState: GlobalState<S, R>;
}>)
{
// Get the current global state provider.
const Provider = useGlobalStateProvider(globalState);
return (
// Render the children inside the provider.
<Provider>{children}</Provider>
);
}
/**
* Get the global state and its reducers.
* @param globalState The global state to get.
*/
export function useGlobalState<S, R extends GlobalStateReducers<S>>(globalState: GlobalState<S, R>): [S, R]
{
// Get the global state data (from its context state).
const ctx = useContext(globalState.getContext());
// Return the context state (global state data), and the global state reducers for this current state.
return [ctx, globalState.getStateReducers(ctx)];
}
/**
* Get the reducers of the global state.
* @param globalState The global state for which to get the reducers.
*/
export function useGlobalStateReducers<S, R extends GlobalStateReducers<S>>(globalState: GlobalState<S, R>): R
{
// Return the global state reducers for the current global state.
return globalState.getStateReducers(useContext(globalState.getContext()));
}
/**
* Get the global state data.
* @param globalState The global state for which to get the data.
*/
export function useGlobalStateValue<S, R extends GlobalStateReducers<S>>(globalState: GlobalState<S, R>): S
{
// Return the global state data (from its context state).
return useContext(globalState.getContext());
}

View file

@ -1,3 +1,4 @@
import {useEffect, useRef} from "react";
export type Modify<T, R> = Omit<T, keyof R> & R; export type Modify<T, R> = Omit<T, keyof R> & R;
@ -31,3 +32,21 @@ export function normalizeString(str: string): string
.replace?.(/[\u0300-\u036f]/g, "") .replace?.(/[\u0300-\u036f]/g, "")
: ""; : "";
} }
/**
* Get the previous value of a given value.
* @param currentValue The current value.
*/
export function usePreviousValue<T>(currentValue: T): T
{
// Get the reference to the previous value.
const ref = useRef<T>();
// If the value has changed, saving it in the reference after rendering.
useEffect(() => {
ref.current = currentValue;
}, [currentValue]);
// Get the previous value.
return ref?.current ?? undefined;
}