Skip to content

Instantly share code, notes, and snippets.

@7iomka
Created September 5, 2023 23:00
Show Gist options
  • Save 7iomka/25f215a3cf41b0ec2c195b62de5495a8 to your computer and use it in GitHub Desktop.
Save 7iomka/25f215a3cf41b0ec2c195b62de5495a8 to your computer and use it in GitHub Desktop.
Factory generic issue
// CAUTION
// - Avoid dynamic factories calls
// - Generic factories are not supported in `useModel`and `modelView` components
// Usage:
// const Root = modelView(factory, () => { ... })
// const model = useModel(factory)
// ... const $$instance = invoke(createSome, {someParams})
'use client';
import { createFactory } from '@withease/factories';
import type { invoke } from '@withease/factories';
import type { ComponentType, Context, Provider } from 'react';
import { createContext, useContext } from 'react';
const contexts = new Map<
ReturnType<typeof createFactory>,
Context<ReturnType<typeof invoke<ReturnType<typeof createFactory>>>>
>();
export const createModelProvider = <T extends (props: any) => any>(factory: T) => {
contexts.set(factory, createContext(null));
return contexts.get(factory)!.Provider as Provider<ReturnType<typeof invoke<T>>>;
};
export const useModel = <T extends (props: any) => any>(factory: T) => {
const model = useContext(contexts.get(factory)!);
if (!model) {
throw new Error('No model found');
}
return model as ReturnType<T>;
};
// Helper type for model prop
export type FactoryModelType<T extends (props: any) => any> = ReturnType<typeof invoke<T, any>>;
// declare function invoke<C extends (...args: any) => any>(factory: C): OverloadReturn<void, OverloadUnion<C>>;
// declare function invoke<C extends (...args: any) => any, P extends OverloadParameters<C>[0]>(factory: C, params: P): OverloadReturn<P, OverloadUnion<C>>;
/**
* HOC that wraps your `View` into model `Provider`. Also adds `model` prop that will be passed into `Provider`
* @param factory Factory that will be passed through Context
* @param View Root component that will be wrapped into Context
* @returns Wrapped component
*/
export const modelView = <T extends (props: any) => any, Props extends object = object>(
factory: T,
View: ComponentType<Props>,
) => {
const Provider = createModelProvider(factory);
const Render = ({ model, ...restProps }: Props & { model: FactoryModelType<T> }) => {
return (
<Provider value={model}>
<View {...(restProps as Props)} />
</Provider>
);
};
// `as` is used for a better "Go To Definition"
return Render as ComponentType<Props & { model: ReturnType<T> }>;
};
export { createFactory };
<CreateSortingSelect
model={$$cartSorting}
useColumnLayoutOnMobile
/>
{/* We have issue here: */}
type ProductItemSortingType =
| 'assign_desc'
| 'assign_asc'
| 'subcategory_desc'
| 'subcategory_asc';
const $$cartSorting = invoke(() =>
createSortingModel<ProductItemSortingType>({
sortingOptions: [
{ label: 'По дате добавления ↑', value: 'assign_asc' },
{ label: 'По дате добавления ↓', value: 'assign_desc' },
{ label: 'По названию категории ↑', value: 'subcategory_asc' },
{ label: 'По названию категории ↓', value: 'subcategory_desc' },
],
defaultValue: 'assign_desc',
queryKey: 'sorting',
}),
);
import { useUnit } from 'effector-react';
import type { MantineNumberSize } from '@mantine/core';
import { Select, useInlineOutsideSelectLabelStyles } from '@/shared/ui';
import type { SelectProps } from '@/shared/ui';
import { modelView, useModel } from '@/shared/lib/factory';
import { factory } from '../sorting.model';
interface CreateSortingSelectProps extends Omit<SelectProps, 'withinPortal' | 'data' | 'onChange'> {
className?: string;
labelSpacing?: MantineNumberSize;
useColumnLayoutOnMobile?: boolean;
}
export const CreateSortingSelect = modelView(factory, (props: CreateSortingSelectProps) => {
const {
className,
label = 'Сортировать',
hasFloatingLabel,
labelSpacing,
useColumnLayoutOnMobile,
...rest
} = props;
const model = useModel(factory);
const [onChange, value] = useUnit([model.valueChanged, model.$value]);
const options = model.sortingOptions;
const { classes } = useInlineOutsideSelectLabelStyles({
size: props.size ?? 'md',
labelSpacing: props.labelSpacing,
useColumnLayoutOnMobile: props.useColumnLayoutOnMobile,
});
return (
<Select
className={className}
classNames={classes}
value={value}
onChange={(v: string) => onChange(v)}
data={options}
withinPortal
label={label}
hasFloatingLabel={hasFloatingLabel ?? false}
{...rest}
/>
);
});
import type { Event, Store } from 'effector';
import { is, createStore, attach, createEvent, sample } from 'effector';
import type { SelectItem } from '@mantine/core';
import { createFactory } from '@withease/factories';
import { $$navigation } from '@/entities/navigation';
export type SortingOption<K extends string> = Omit<SelectItem, 'label' | 'value'> & {
label: string;
value: K;
};
type FactoryOptions<K extends string = string> = {
sortingOptions: SortingOption<K>[];
queryKey?: 'sorting' | string;
defaultValue?: SortingOption<K>['value'] | null;
getInitialValueOn?: Event<any | void> | Event<any | void>[];
syncQueryParams?: boolean;
isEnabled?: Store<boolean> | boolean;
};
export const factory = createFactory(
<K extends string>({
sortingOptions,
queryKey = 'sorting',
defaultValue: _defaultValue = null,
getInitialValueOn,
syncQueryParams,
isEnabled,
}: FactoryOptions<K>) => {
const availableValues = sortingOptions.map((opt) => opt.value);
const $isEnabled = is.store(isEnabled) ? isEnabled : createStore(isEnabled ?? false);
// Allow null as default value, with optional fallback to `default`
const defaultValue = _defaultValue ?? ('default' in availableValues ? ('default' as K) : null);
// type SortingValue = (typeof sortingOptions)[number]['value'];
const initialValueSettled = createEvent<K | null>();
const valueChanged = createEvent<K>();
const urlPrepared = createEvent<{ url: string }>();
const urlChangeRequested = createEvent<{ url: string }>();
const urlChanged = createEvent<{ url: string }>();
const changeUrlFx = attach({ effect: $$navigation.pushFx });
// store for current sorting
const $value = createStore(defaultValue);
// Reflect value changes by init event
sample({
clock: initialValueSettled,
target: $value,
});
// Reflect value changes by event
sample({
clock: valueChanged,
target: $value,
});
/**/
return {
sortingOptions,
queryKey,
defaultValue,
$value,
valueChanged,
urlChangeRequested,
urlChanged,
initialValueSettled,
};
},
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment