Skip to content

Instantly share code, notes, and snippets.

@dralletje
Created April 13, 2020 14:58
Show Gist options
  • Save dralletje/508f129ac0d4a77f33b161fff7b76608 to your computer and use it in GitHub Desktop.
Save dralletje/508f129ac0d4a77f33b161fff7b76608 to your computer and use it in GitHub Desktop.
// @flow
import React from 'react';
import shallowEqual from 'shallowequal';
import { debounce } from 'lodash';
import immutagen from 'immutagen';
export let create_generator_component = (generator_fn) => {
let { Provider, Consumer } = React.createContext();
let immutable_generator = immutagen(generator_fn);
let render_subchild = (generator, value) => {
let { value: element, next: next_generator } = generator(value);
return <Provider value={{ generator: next_generator }}>{element}</Provider>;
};
let YieldRender = (props) => {
return render_subchild(immutable_generator, {
props: props,
resolve: (value) => {
return (
<Consumer>
{({ generator }) => render_subchild(generator, value)}
</Consumer>
);
},
});
};
return YieldRender;
};
export let generator_render = (generator_fn) => {
let { Provider, Consumer } = React.createContext();
let render_subchild = (generator, value) => {
let { value: element, next: next_generator } = generator(value);
return <Provider value={{ generator: next_generator }}>{element}</Provider>;
};
function render() {
let element_this = this || {
get props() {
throw new Error(`Trying to get this.props, but this render function is not bound to a react component :O`);
},
get state() {
throw new Error(`Trying to get this.state, but this render function is not bound to a react component :O`);
}
};
let bound_generator_fn = (...args) => generator_fn.call(element_this, ...args);
let immutable_generator = immutagen(bound_generator_fn);
let resolve = (value) => {
return (
<Consumer>
{({ generator }) => render_subchild(generator, value)}
</Consumer>
);
};
return render_subchild(immutable_generator, resolve);
};
return render;
}
// Still not sure what I should call this
export class EmptyC extends React.Component {
render() {
return null;
}
}
export class RenderFunction extends React.Component {
render() {
return this.props.render(this.props);
}
}
export class State extends React.Component {
/*:flow
props: {
initialValue: any,
children?: (value: any, update: (value: any) => void) => React$Element<*>,
};
*/
state = {
thing: this.props.initialValue,
};
render() {
if (!this.props.children) return;
return this.props.children(this.state.thing, (valueOrFn) => {
if (typeof valueOrFn === 'function') {
this.setState((state) => {
return { thing: valueOrFn(state.thing) };
});
} else {
if (!shallowEqual(this.state.thing, valueOrFn)) {
this.setState({ thing: valueOrFn });
}
}
});
}
}
export let GoodState = ({ initialValue, children, merge }) => {
return (
<State
initialValue={initialValue}
children={(state, set_state) =>
children({
state,
set_state: merge
? (new_state) =>
set_state((old_state) => {
return { ...old_state, ...new_state };
})
: set_state,
})
}
/>
);
};
export class Compose extends React.Component {
props: {
[key: string]: (children: () => React$Element<*>) => React$Element<*>,
children?: () => React$Element<*>,
};
render() {
const { children, ...chain } = this.props;
const entries = Object.entries(chain);
const fn = entries.reduce((acc, [key, wrapFn]) => {
// $FlowFixMe
return (props) => wrapFn((value) => acc({ ...props, [key]: value }));
}, (props) => (children ? children(props) : null));
return fn();
}
}
type TCleanUp = () => mixed;
export class Lifecycle extends React.Component {
cleanup: ?TCleanUp;
props: {
componentDidMount?: () => TCleanUp | mixed,
componentWillUnmount?: () => mixed,
};
componentDidMount() {
if (this.props.componentDidMount) {
let cleanup = this.props.componentDidMount();
if (typeof cleanup === 'function') {
this.cleanup = cleanup;
}
}
}
componentWillUnmount() {
if (this.props.componentWillUnmount) this.props.componentWillUnmount();
if (this.cleanup) this.cleanup();
}
render() {
return this.props.children || null;
}
}
type TOnChangeProps = {
values: any,
onUpdate: (props: any) => mixed,
};
export class OnChange<T: any> extends React.Component {
props: TOnChangeProps;
componentDidMount() {
this.props.onUpdate(this.props.values);
}
componentDidUpdate(prevProps: TOnChangeProps) {
if (!shallowEqual(prevProps.values, this.props.values)) {
this.props.onUpdate(this.props.values);
}
}
render() {
return null;
}
}
// TODO Add something that prevents page navigation (react router prompt?) while it is updating
export class BufferedField extends React.Component {
/*:flow
props<T>: {
value: T,
onCommit: (value: T) => mixed,
commitEvery: number, // Milliseconds
children: (value: T, set_value: (T) => mixed) => React$Element,
}
*/
state = {
buffered_value: this.props.value,
last_commited_value: this.props.value,
};
debounced_commit = debounce(() => {
this.props.onCommit(this.state.buffered_value);
this.setState({
last_commited_value: this.state.buffered_value,
});
}, this.props.commitEvery);
static getDerivedStateFromProps(props, state) {
if (props.value !== state.last_commited_value) {
return {
buffered_value: props.value,
last_commited_value: props.value,
};
} else {
return null;
}
}
componentWillUnmount() {
// Force the pending update
this.debounced_commit.flush();
}
render() {
return this.props.children(this.state.buffered_value, (new_value) => {
this.setState({ buffered_value: new_value });
this.debounced_commit();
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment