veest v1.0.17
DO NOT USE IT IN PROD. THIS IS EXPERIMENTAL
Hook based, dependency injectable state manager for React
Installation
yarn add veest
Motivation
- No need to learn a bunch of new API, only plain react-hooks knowledge needed;
- Dependency injectable. Easy to mock, test and use;
- No bulky boilerplate code needed for just running "hello world app";
- No magic. No "easy to learn, hard to master".
The reason why
Have you ever felt sad about this situation?
- You need to test or somehow reuse a component tied to a store, and neither you nor typescript knows what data is required by this component.
// Outer -> connect(Inner) -> connect(Inner1) -> connect(SomethingInner2)
const App = () => (
<>
<Outer/> -> doesn't work outside. Typescript doesn't know why
<StoreProvider store={store}>
<Outer/> -> works fine placed inside a store provider
</StoreProvider>
</>
)
With veest typescript always knows what data is required by your component.
// const Bootstrap = container( // inject<SomeService>()('someService'), // inject<SomeOtherService>()('someOtherService'), // () => ... // ) // const Outer = Bootstrap({someService, someOtherService}) const App = () => ( <> <ServiceContainer/> <Outer/> -> works everywhere because everything is injected </> ) // Only restriction - ServiceContainer is the place where all Services running, so you should render it at the top of your app, or anywhere you want to run your container (e.g. tests).
Quick example
import { useState, useEffect, useMemo } from 'react';
import { configure, watch, useAction } from 'veest';
const { ServiceContainer, service, inject, conatiner } = configure();
const style = { background: 'antiquewhite', borderRadius: '10px', padding: '10px 0' };
type AppService = { loading: boolean }
const createAppService = service((): AppService => {
const [loading, setLoading] = useState(true);
const logLoading = useAction(() => {
console.log(loading);
})
useEffect(() => {
setTimeout(() => setLoading(false), 1000);
}, []);
return { loading, logLoading }
})
type UserService = { name: string }
const createUserService = service(
inject<AppService>()('appService'),
(useAppService): UserService => {
const {loading} = useAppService();
const name = loading ? '...' : 'Your name';
return { name }
}
)
type SidebarProps = { weather: string }
const Sidebar = container(
inject<UserService>()('userService'),
(useUserService) => (props: SidebarProps) => {
const {name} = useUserService();
return <div style={style}>Username: {name}. Weather: {props.weather}</div>
}
)
const Main = container(
inject<AppService>()('appService'),
Sidebar,
(useAppService, Sidebar) => () => {
const {loading} = useAppService();
return <div style={style}>{loading ? 'App is loading' : 'Loaded!'} <Sidebar weather="sunny" /></div>
}
)
const appService = createAppService();
const userService = createUserService({ appService });
const ContainerResolved = Main({ appService, userService });
watch(services, { showCurrent: true, showPrevious: true, showDiff: true, storeInWindow: true });
const App = () => (
<>
<ServiceContainer/>
<ContainerResolved/>
</>
)
ReactDOM.render(
<App/>,
document.getElementById('root')
)
More examples
Ideology
No context anymore! Everything your component needs is injectable. First, let's differentiate our composites into 3 groups.
- Component - renders small pieces of UI (aka presentation components, dumb components);
- Service - have some data or data+handlers (aka data layer);
- Container - combines injected data with presentational components (and some layout).
Code pasted from example above.
Containers need to be resolved to use. Services can also be resolved with other Services and be independent.
Service body is a plain React hook, so you can use anything with HookAPI inside it. (GraphQL, UseQuery, Formik, ...)
You can create an independent Service const createAppService = service(...)
, and to resolve it you need to call returned function const appService = createAppService();
;
You can create a dependent Service const createUserService = service(...)
and require other Service of type AppService
be injected by key appService
to use it const createUserService = service(...)
;
Last parameter is always a "product" which takes all dependencies and returns new Service (useAppService): UserService => {
;
Container API is almost same, but it should require at least one Service, or other Container to resolve;
Last parameter "product" also takes all dependencies, but now it should return a function of type FunctionComponent
with props you want to pass (useUserService) => (props: SidebarProps) => {
.
When Container A, depending on {t: T}, runs through itself another Container B, depending on {r: R}, the result deps will be {t: T} & {r: R} const ContainerResolved = Main({ appService, userService });
.
In order to resolve Service or Container, just call returned function with all needed dependencies: const userService = createUserService({ appService });
, const ContainerResolved = Main({ appService, userService });
.
watch(services, options)
Watch services changing in the console.
[options.showCurrent=true]
. If enabled - logs current service data[options.showPrevious=false]
. If enabled - logs previous service data[options.showDiff=false]
. If enabled - logs diff between previous and current service data[options.storeInWindow=true]
. If enabled - stores services inside of window object
useAction
It's used for handlers memoization like a useCallback hook, but the reference to a resulting function gets never updated;
import {useAction} from 'veest';
const Component = () => {
const [count, setCount] = useState(0);
const memoizedWithUseCallback = useCallback(() => {
// do whatever you want with count
console.log(count)
}, [count]);
const memoizedWithUseAction = useAction(() => {
// do whatever you want with count
console.log(count)
}); // takes only callback
// if you compare memoizedWithUseCallback and memoizedWithUseAction functions on every rerender
// you see that reference to memoizedWithUseAction never gets updated
// but always has actual data inside of callback
return <div>
<button onClick={() => setCount(prev => prev+1)}>increase</button>
<button onClick={memoizedWithUseCallback}>memoizedWithUseCallback</button>
<button onClick={memoizedWithUseCallback}>memoizedWithUseCallback</button>
</div>
}
ServiceContainer
Pay attention, that returned ServiceContainer is the place where all Services running, so you should render it at the top of your app, or anywhere you want to run your container (e.g. tests).
configure()
[options.proxy=true]
. If enabled - the returned value from injected Service will be intercepted to see what properties you useconst {loading} = useAppService();
and will NOT cause rerender if these properties not changed. So whether you want to get props from it, you should use destructuring at the top.
[options.memo=true]
. If enabled - every resolved container will be wrapped withReact.memo
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago