React hooks solves the issues this library set out to solve. It will not be maintained going forward.
A stateful, functional React component microlibrary.
Fuck this
is a microlibrary for creating stateful, functional React components.
It's inspired (via an ajar window) by the approaches taken in ReasonReact and Hyperapp.
It looks like this:
import { stateComponent } from 'fuck-this';
const initialState = { count: 0 };
const reducers = {
increment: (state, amount = 1) => ({
...state,
count: state.count + amount
})
};
const render = ({ count, increment }) => (
<>
<Text>{count}</Text>
<Button onPress={() => increment()}>+</Button>
</>
);
export default stateComponent(initialState, reducers, render);
- Redux-style reducers: Use of Redux-style pure reducer functions means all state logic neatly resides in one testable location.
- Reducers => actions: Reducers are automatically converted into action functions and passed in as props. Named reducers means no more action strings littering your codebase.
- Async reducers: Reducers can be asynchronous to resolve data from external sources.
- Portable render: As state and actions are passed as props, the render function remains portable and independent from the library.
- Context: Share state and actions throughout an app using
stateContext
. - Tiny: Less than 1kb.
Fuck this
can be installed via your favourite package manager as fuck-this
:
npm install fuck-this
You can also use Fuck this
in CodePen or locally by including the script from https://unpkg.com/fuck-this@0.1.0
.
To cover the basics of Fuck this
, let's create a simple counter component with stateComponent
. You've probably made a few of these in your time, one more won't hurt (we hope).
State is defined as a plain object:
const initialState = { count: 0 };
Each property defined in the state will be provided to our render function as a prop, e.g:
({ count }) => <span>{count}</span>;
A reducer is a function that accepts the current state and, optionally, a data payload. It returns a new copy of the state.
const increment = (state, payload) => ({
...state,
count: state.count + payload
});
In Fuck this
, reducers are defined as a named map of functions:
const reducers = { increment };
Each named reducer will have a corresponding action generated and these will be passed to the render function as a prop.
Actions are called with just the payload
argument. Fuck this
will fire the corresponding reducer with the latest state
and the provided payload
.
({ increment }) => <button onClick={() => increment(1)}>+</button>;
The final piece of a stateful component is the functional render component.
Create a component that accepts both counter
and increment
as props:
const render = ({ count, increment }) => (
<>
<span>{count}</span>
<button onClick={() => increment(1)}>+</button>
</>
);
Now we have our initial state, our reducers and a render function. Use them to create a stateful React component using the stateComponent
function:
import { stateComponent } from 'fuck-this';
/* Our `initialState`, `reducers` and `render` declarations */
export default stateComponent(initialState, reducers, render);
This component can be now be rendered as any other:
import Counter from './Counter';
export default () => <Counter />;
So far we've defined initialState
and reducers
as objects.
Both can also be defined as a function that returns an object. This function is provided the props
that are provided to the component by its parent when it's mounted:
const initialState = ({ initialCount }) => ({ count: initialCount });
// Later...
() => <Counter initialCount={1} />;
count
will now be initialised as 1
.
With reducers, we can use this capability to (for instance) bind an API endpoint. But if we're calling an async endpoint, we'll need async reducers. Which, luckily...
Reducers can be asynchronous. They must still return a new state, but can do so after resolving data, or some other async operation.
Here's a reducers
factory function that takes a super-useful fetchCount
endpoint via component props
, and returns an async updateCount
reducer:
const initialState = { count: 0 };
const reducers = ({ fetchCount }) => ({
updateCount: async state => {
const count = await fetchCount();
return { ...state, count };
}
});
The rest of our component looks like this:
const render = ({ count, updateCount }) => (
<>
<span>{count}</span>
<button onClick={updateCount}>Update</button>
</>
);
export default stateComponent(initialState, reducers, render);
We'd call this component like so, ensuring we provide a fetchCounter
function:
<Counter fetchCounter={fetchCounter} />
This async example calls the endpoint but doesn't yet communicate via state that loading is in progress.
Each action returns a Promise
, so we can compose actions like this:
indicateUpdating().then(fetchCounter);
Add isUpdating
to the state:
const initialState = {
count: 0,
isUpdating: false
};
Add a reducer to set it to true
, and return isUpdating: false
from the updateCount
reducer:
const reducers = ({ fetchCount }) => ({
indicateUpdating: state => ({ ...state, isUpdating: true }),
updateCount: async state => {
const count = await fetchCount();
return {
...state,
count,
isUpdating: false
};
}
});
Now we amend the render
function to 1) disable the update button if isUpdating
is true
, and 2) to fire indicateUpdating
before updateCount
:
const render = ({ count, updateCount, indicateUpdating, isUpdating }) => (
<>
<span>{count}</span>
<button
disabled={isUpdating}
onClick={() => indicateUpdating().then(updateCount)}
>
Update
</button>
</>
);
stateComponent
is great for creating components with local state, but sometimes we need a way to pass this state throughout our application.
The stateContext
function does exactly this. It takes the same initialState
and reducers
arguments as stateComponent
, and returns a Provider
component to provide the state and actions to children, and a consumer
function to provide these as props to a render
function.
Create a stateContext
with the same initialState
and reducers
that we used earlier:
import { stateContext } from 'fuck-this';
const initialState = { count: 0 };
const reducers = {
increment: (state, amount) => ({
...state,
count: state.count + amount
})
};
export default stateContext(initialState, reducers);
To provide this state to a subtree, we can use the Provider
component that this function generates.
It works just like React.createContext
's Provider
, except our state and actions have already been generated so there's no need to set value
:
import Counter from './Counter';
export default () => (
<Counter.Provider>
{/* Any children can subscribe to this store via Counter.consume */}
</Counter.Provider>
);
This component can optionally accept props which can be used to initiate initialState
and/or reducers
as before.
<Counter.Provider initialCount={1}>
To subscribe a functional component to this state, provide it to the generated consume
function:
import Counter from './Counter';
const render = ({ count, increment }) => (
<>
<span>{count}</span>
<button onClick={() => increment(1)}>+</button>
</>
);
export default Counter.consume(render);
Used to create a stateful component.
import { stateComponent } from 'fuck-this';
type Props = { [key: string]: any };
type State = { [key: string]: any };
type Reducers = {
[key: string]: (state: State, payload: any) => State
};
type StateComponent = (
initialState: State | (props: Props) => State,
reducers: Reducers | (props: Props) => Reducers,
render: React.PureComponent | (props: Props) => React.Node
) => React.Component;
Used to create a state Provider
component and consume
higher-order component.
import { stateContext } from 'fuck-this';
type Props = { [key: string]: any };
type State = { [key: string]: any };
type Reducers = {
[key: string]: (state: State, payload: any) => State
};
type StateContext = (
initialState: State | (props: Props) => State,
reducers: Reducers | (props: Props) => Reducers
) => {
Provider: React.Component,
consume: (component: React.Component) => React.Component
};
Fuck this
provides some utility methods to make creating reducers for common data types simpler.
A function that will create a reducer that toggles a binary state.
import { toggle } from 'fuck-this';
const initialState = { isOpen: false };
const reducers = { toggleOpen: toggle('isOpen') };
(key: string) => (state: State) => State;
Some ideas on how to push this library/pattern forward:
- Lifecycle events: If we can expose lifecycle events in a pure way, this might be a nice extra. I personally feel like ReasonReact's
self
is too much of an encroachment of classiness into FP, but there's probably a pure solution here. It is currently possible to provide aPureComponent
as theComponent
argument, so no rush. - State merging: Currently, a reducer needs to return a full copy of the state.
this.setState
has the ability to define only a portion of the state, which will be shallow-merged with the existing state. A possibility? - Action composition: Currently, to call reducers in sequence we need to chain them via
await
calls or promises. There's probably a better way to compose these. I played with providing actions as a third argument to reducers, but I feel like it takes away from their purity.
Does this work with React < 16.3?
No. stateContext
uses the new context API. It's a tough old world I know but it's just a simple matter of not having time to support legacy environments.
Browser support?
Absolutely.
Why "Fuck this
"?
In my opinion, there's a few problems with this
and, specifically, setState
.
With this
in general I feel like the whole mess around execution context and needing to ensure we've bound certain functions in certain situations is indicative that there's a better, simpler, cleaner way.
This isn't to say that this
doesn't have its virtues, just that my gut feel is that it's overused in a library that is state => view
.
As for setState
, it's messy and unpredictable.
It's fine(ish) for simple tasks but larger components can quickly grow littered with them and it can be hard to know what each one does. It's only up to convention that they're wrapped in a function that clearly says what it does.
Each setState
call can modify the state in any way it wishes. It can be hard to track down the source of changes.
With reducers, we keep all of our state dickery into a single place. The reducers themselves are pure, so we can test that they do exactly what they say they will. And because they're named, we know exactly what each one is supposed to do.
No, I meant the "fuck". Do you honestly expect me to use this in a professional environment?
Haha. IKR.