Skip to content

Instantly share code, notes, and snippets.

@adamziel
Last active December 15, 2021 14:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adamziel/2ee2a22b417825e9324f9dad26c17e73 to your computer and use it in GitHub Desktop.
Save adamziel/2ee2a22b417825e9324f9dad26c17e73 to your computer and use it in GitHub Desktop.

Gutenberg 11.6 added supports for thunks. You can think of thunks as of functions that can be dispatched:

// actions.js
export const myThunkAction = () => ( { select, dispatch } ) => {
	return "I'm a thunk! I can be dispatched, use selectors, and even dispatch other actions.";
};

Why are thunks useful?

Thunks expand the meaning of what a Redux action is. Before thunks, actions were purely functional and could only return and yield data. Common use-cases such as interacting with the store or requesting API data from an action required using a separate control. You would often see code like:

export function* saveRecordAction( id ) {
	const record = yield controls.select( 'current-store', 'getRecord', id );
	yield { type: 'BEFORE_SAVE', id, record };
	const results = yield controls.fetch({ url: 'https://...', method: 'POST', data: record });
	yield { type: 'AFTER_SAVE', id, results };
	return results;
}

const controls = {
	select: // ...,
	fetch: // ...,
};

Side-effects like store operations and fetch functions would be implemented outside of the action. Thunks provide alternative to this approach. They allow you to use side-effects inline, like this:

export const saveRecordAction = ( id ) => async ({ select, dispatch }) => {
	const record = select( 'current-store', 'getRecord', id );
	dispatch({ type: 'BEFORE_SAVE', id, record });
	const response = await fetch({ url: 'https://...', method: 'POST', data: record });
	const results = await response.json();
	dispatch({ type: 'AFTER_SAVE', id, results });
	return results;
}

This removes the need to implement separate controls.

Thunks have access to the store helpers

Let's take a look at an example from Gutenberg core. Prior to thunks, the toggleFeature action from the @wordpress/interface package was implemented like this:

export function* toggleFeature( scope, featureName ) {
	const currentValue = yield controls.select(
		interfaceStoreName,
		'isFeatureActive',
		scope,
		featureName
	);

	yield controls.dispatch(
		interfaceStoreName,
		'setFeatureValue',
		scope,
		featureName,
		! currentValue
	);
}

Controls were the only way to dispatch actions and select data from the store.

With thunks, there is a cleaner way. This is how toggleFeature is implemented now:

export function toggleFeature( scope, featureName ) {
	return function ( { select, dispatch } ) {
		const currentValue = select.isFeatureActive( scope, featureName );
		dispatch.setFeatureValue( scope, featureName, ! currentValue );
	};
}

Thanks to the select and dispatch arguments, thunks may use the store directly without the need for generators and controls.

Thunks may be async

Imagine a simple React app that allows you to set the temperature on a thermostat. It only has one input and one button. Clicking the button dispatches a saveTemperatureToAPI action with the value from the input.

If we used controls to save the temperature, the store definition would look like below:

const store = wp.data.createReduxStore( 'my-store', {
    actions: {
        saveTemperatureToAPI: function*( temperature ) {
            const result = yield { type: 'FETCH_JSON', url: 'https://...', method: 'POST', data: { temperature } };
            return result;
        }
    },
    controls: { 
        async FETCH_JSON( action ) {
            const response = await window.fetch( action.url, {
                method: action.method,
                body: JSON.stringify( action.data ),
            } );
            return response.json();
        }
    },
    // reducers, selectors, ...
} );

While the code is reasonably straightforward, there is a level of indirection. The saveTemperatureToAPI action does not talk directly to the API, but has to go through the FETCH_JSON control.

Let's see how this indirection can be removed with thunks:

const store = wp.data.createReduxStore( 'my-store', {
    __experimentalUseThunks: true,
    actions: {
        saveTemperatureToAPI: ( temperature ) => async () => {
            const response = await window.fetch( 'https://...', {
                method: 'POST',
                body: JSON.stringify( { temperature } ),
            } );
            return await response.json();
        }
    },
    // reducers, selectors, ...
} );

That's pretty cool! What's even better is that resolvers are supported as well:

const store = wp.data.createReduxStore( 'my-store', {
    // ...
    selectors: {
        getTemperature: ( state ) => state.temperature
    },
    resolvers: {
        getTemperature: () => async ( { dispatch } ) => {
            const response = await window.fetch( 'https://...' );
            const result = await response.json();
            dispatch.receiveCurrentTemperature( result.temperature );
        }
    },
    // ...
} );

Thunks' support is experimental for now. You can enable it by setting __experimentalUseThunks: true when registering your store.

Thunks API

A thunk receives a single object argument with the following keys:

select

An object containing the store’s selectors pre-bound to state, which means you don't need to provide the state, only the additional arguments. select triggers the related resolvers, if any, but does not wait for them to finish. It just returns the current value even if it's null.

If a selector is part of the public API, it's available as a method on the select object:

const thunk = () => ( { select } ) => {
    // select is an object of the store’s selectors, pre-bound to current state:
    const temperature = select.getTemperature();
}

Since not all selectors are exposed on the store, select doubles as a function that supports passing selector as an argument:

const thunk = () => ( { select } ) => {
    // select supports private selectors:
    const doubleTemperature = select( ( temperature ) => temperature * 2 );
}

resolveSelect

resolveSelect is the same as select, except it returns a promise that resolves with the value provided by the related resolver.

const thunk = () => ( { resolveSelect } ) => {
    const temperature = await resolveSelect.getTemperature();
}

dispatch

An object containing the store’s actions

If an action is part of the public API, it's available as a method on the dispatch object:

const thunk = () => ( { dispatch } ) => {
    // dispatch is an object of the store’s actions:
    const temperature = await dispatch.retrieveTemperature();
}

Since not all actions are exposed on the store, dispatch doubles as a function that supports passing a redux action as an argument:

const thunk = () => async ( { dispatch } ) => {
	// dispatch is also a function accepting inline actions:
	dispatch({ type: 'SET_TEMPERATURE', temperature: result.value });
    
	// thunks are interchangeable with actions
	dispatch( updateTemperature( 100 ) );
	
	// Thunks may be async, too. When they are, dispatch returns a promise
	await dispatch( ( ) => window.fetch( /* ... */ ) );
}

registry

Registry provides access to other stores through dispatch, select, and resolveSelect methods. They are very similar to the ones described above, with a slight twist. Calling registry.select( storeName ) returns a function returning an object of selectors from storeName. This comes handy when you need to interact with another store. For example:

const thunk = () => ( { registry } ) => {
  const error = registry.select( 'core' ).getLastEntitySaveError( 'root', 'menu', menuId );
  /* ... */
}
@jsnajdr
Copy link

jsnajdr commented Sep 30, 2021

Some comments:

What is a thunk?
First of all, I think the motivation for thunks can be explained in a much simpler language than some very fancy-sounding "asynchronous side-effects". A baseline store has certain primitives: selectors and actions of the { type: FOO } kind that update the state through the reducer and nothing else. Now the demand for something like thunks comes when we want to compose these primitives into something more complex.

We know that JavaScript functions can be composed by calling other functions -- that's a complete no-brainer for everyone:

function complexOp( input ) {
  const a = simpleOp1( input );
  simpleOp2( input, a );
}

Now how's that related to thunks? Consider the simplest thunk I've seen in Gutenberg so far, the toggleFeature action in @wordpress/interface:

const toggleFeature = ( featureName ) => ( { select, dispatch } ) => {
  const currentValue = select.isFeatureActive( featureName );
  dispatch.setFeatureValue( featureName, ! currentValue );
}

There's nothing asynchronous here, and yet it's a very nice thunk. Its entire job is to compose two primitives together, and be indistinguishable from a primitive action.

Just like we were able to create complexOp from simpleOps and it's still a function, primitive actions and thunks are also both actions:

dispatch.setFeatureValue( 'gallery', true );
dispatch.toggleFeature( 'gallery' );

The consumer doesn't know which one is a primitive { type: SET, feature: 'gallery', value: true } object and which is a thunk. Both quack like actions.

Now the ( { select, dispatch } ) bit is just a smart way to do dependency injection. The toggleFeature doesn't know which registry and store it will be running on, just like the { type: SET } action object doesn't know anything. It will be injected when the store executes the action.

What's the main point of rungen generators?
A generator, executed alone, never actually does anything directly, like sending a request with window.fetch. Instead of that, it uses the Command design pattern (btw does anyone still know and read the classic GoF books in 2021?) to generate command objects like

{ type: 'FETCH_JSON', url: 'https://example.api/temperature' }

and only then the rungen runtime executes a control that has been registered with it, which does the actual window.fetch call.

That's an extra level of indirection and abstraction, and it turns out we almost never need that. We'd like to simplify things by calling window.fetch directly.

With thunks you can indeed call window.fetch directly, and removing the indirection should make your code simpler. It's just functions calling other functions, without any runtime that interprets commands.

retrieveTemperature should be a resolver
My final bit of feedback is that the example with retrieveTemperature is not very good because retrieveTemperature is a typical resolver, it shouldn't be an action. A more realistic example for an useful async thunk would some mutation action, one that POSTs something to the REST server.

@adamziel
Copy link
Author

adamziel commented Sep 30, 2021

All good notes @jsnajdr! For some context, I aimed for consistency by using the same language and examples as the Gutenberg documentation. The temperature bit comes from https://developer.wordpress.org/block-editor/reference-guides/packages/packages-redux-routine/ and the "asynchronous side-effects" part is from https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/#comparison-with-redux.

I agree this piece could do better, though. I'll simplify it. It looks like an opportunity to improve the docs, too.

@jsnajdr
Copy link

jsnajdr commented Sep 30, 2021

I know, talking about thunks as "asynchronous side-effects" is quite popular, even in the official redux-thunk docs and debates. But I think that describing thunks from a completely different angle, as a mechanism for composing primitives into more complex units, is in some sense much better and more simple. Although async fetching is probably the vast majority of use cases.

@adamziel
Copy link
Author

adamziel commented Oct 4, 2021

@jsnajdr I updated to use a more approachable language and hopefully better examples. I want to take one more pass, but I wonder if you have any directional feedback at this point.

@jsnajdr
Copy link

jsnajdr commented Oct 5, 2021

@adamziel @mcsf I tried to organize my thoughts about thunks and controls and the result are two blog posts:

  • Motivation for Thunks where I argue that thunks are simply a way how to write helper functions that work with a store (select, dispatch) and build more complex actions from primitive actions.
  • What’s the point of generators and controls in @wordpress/data? explaining that generators and controls are a way how to implement a thunk (or any other function) with side-effects done not imperatively, but in a purely functional way, inspired by Haskell or Elm.

Let me know if that makes for an interesting and comprehensible reading and if you think it will be accessible and useful for a wider WordPress community.

@jsnajdr
Copy link

jsnajdr commented Oct 5, 2021

Thunks make a great replacement for controls.

As I argue in my blog post, thunks are a simplification of controls. We give up on the purely functional approach, and start calling the side-effect-ful imperative functions directly again.

select vs resolveSelect: the difference is that select returns the state as it is right now (even if null), and triggers the resolver as a side-effect, while resolveSelect waits until the selector is really resolved.

In addition, select doubles as a function taking inline selectors.

For clarity sake, this would deserve a separate section about private selectors and actions.

If a selector is part of the public API, it's available as a method on the select object: select.getTemperature(). If the selector is private, it's not a method on select, but must be passed to the select function: select( state => state.temp * 2 ). The same for dispatch.

A thunk may return a promise

Control/generator can return a promise, too, but it's very hard to understand what's going on. With thunks is much clearer what is being returned from where, because it's just functions calling other functions, without additional levels of indirection.

@getdave
Copy link

getdave commented Oct 6, 2021

Nice work on this Adam. Just adding some suggestions below as I find them.

A thunk have a direct access to store

A thunk has direct access to a store.

or

Thunks have direct access to the store.

?

A thunk may return a promise

A thunk may return a Promise.

return function ( { select, dispatch } )

Are we destructuring from the store here? If so would it be helpful to use store.select and store.dispatch just for absolutely clarity that the store object is provided to the thunk?

Ok I just read https://gist.github.com/adamziel/2ee2a22b417825e9324f9dad26c17e73#thunks-api so we don't receive the store object directly.

In that case please could you clarify what you mean by "A thunk have a direct access to store"?

resolveSelect

This title resolveSelect under the Thunks API section has lost it's formatting.

pre-bound to state

When you mention this is it worth clarifying that this means the state argument is already applied to the selector function as a convenience in order that you don't need to provide it?

dispatch

This section has no explanation. Perhaps "dispatch is an object of the store’s actions" although perhaps "an object containing the store's actions" might be clearer?

registry

Could we clarify the purpose of/why we might need to use this? I assume it's just on the occasion your state depends on accessing another store then this is how you can do it without importing that store...etc.

btw does anyone still know and read the classic GoF books in 2021

@jsnajdr I am very much aware of it but haven't read for a long time. Probably time to revisit it soon...

@adamziel
Copy link
Author

adamziel commented Oct 6, 2021

Motivation for Thunks where I argue that thunks are simply a way how to write helper functions that work with a store (select, dispatch) and build more complex actions from primitive actions.

@jsnajdr The explanation is great, I like how you put it: "What thunks do is that they expand the meaning of what is a Redux action." May I borrow it for this tutorial?

One note I have is that the example store was a bit long. It took more than one full screen on my 15" Mac and I had troubles following along as you were referring to different parts of it. I wonder if there's a way to contain the same insights in 20-30 lines of code? e.g. resetFeature doesn't seem to add much, the actions creators could maybe be inlined without losing too much readability etc

What’s the point of generators and controls in @wordpress/data? explaining that generators and controls are a way how to implement a thunk (or any other function) with side-effects done not imperatively, but in a purely functional way, inspired by Haskell or Elm.

That was neat, I actually started reading thinking "I don't think generators are actually functional" and finished thinking "ooh that's a neat way of thinking about it". I also liked the note about thunks and generators being two different levels of abstraction. 👍 The only thing I would maybe add is an actual snippet from a purely-functional language, be it Haskell, elm, ocaml, or anything else. I know they tend to look foreign for folks used to JS, but maybe there's a way to craft one that wouldn't?

@jsnajdr
Copy link

jsnajdr commented Oct 7, 2021

May I borrow it for this tutorial?

Sure!

One note I have is that the example store was a bit long.

Thanks for the feedback, I updated the post to reduce the example code from 71 to just 35. By removing the resetFeature action and compressing the line count where possible.

The only thing I would maybe add is an actual snippet from a purely-functional language, be it Haskell

I can try that, although I never really wrote even a single line of Haskell or Elm, not even a Hello World 🙂 My knowledge has been purely theoretical until now. Let's see what I can do.

@adamziel
Copy link
Author

adamziel commented Oct 8, 2021

Thanks for the feedback, I updated the post to reduce the example code from 71 to just 35. By removing the resetFeature action and compressing the line count where possible.

That's much better, thank you!

@adamziel
Copy link
Author

adamziel commented Oct 8, 2021

@jsnajdr @getdave I updated the tutorial to reflect your feedback. I also took the liberty to linking to one of your posts @jsnajdr. Do you think it makes a good job explaining the topic now? Or can I still make it cleaner?

@getdave
Copy link

getdave commented Oct 8, 2021

This reads a lot more clearly now and with the changes to the technical elements, is much easier to understand the benefits 🏅

Some more nits I'm afraid 😞... (bear with me!)

Side-effects like store operations and fetch function would be implemented outside of the action. Thunks provide alternative to this approach. They allow you to use side-effects inline, like this:

Side-effects like store operations and fetch functions would be implemented outside of the action. Thunks provide an alternative to this approach. They allow you to use side-effects inline, like this:

The toggleFeature action from the @wordpress/interface package was implemented like this before thunks:

Prior to thunks, the toggleFeature action from the @wordpress/interface package was implemented like this:

Imagine a simple react app

Imagine a simple React app

In only has one input and one button.

It only has one input and one button.

While the code is reasonably straightforward, there is a level of indirection: The saveTemperatureToAPI acti...

(note the full stop has been added)

While the code is reasonably straightforward, there is a level of indirection. The saveTemperatureToAPI acti...

select triggers the related resolvers, if any, but do not wait for them to finish.

select triggers the related resolvers, if any, but does not wait for them to finish.

resolveSelect is the same as select, except it returns a promise that resolvers with the value provided by the related resolver.

resolveSelect is the same as select, except it returns a promise that resolves with the value provided by the related resolver.

@adamziel
Copy link
Author

adamziel commented Oct 8, 2021

@getdave thank you so much! I just applied these changes, I think we're pretty close to shipping 🤞

@mcsf
Copy link

mcsf commented Oct 8, 2021

Thanks for getting this excellent conversation going, everyone.

I confess that I've been somewhat playing Devil's advocate, because I too am partial towards thunks. A few thoughts:

  • I took a long tour across parts of our state implementation, our documentation, in-house libraries, and archives to understand how we got here and how much of the redux-routine's initial principles still hold.
  • In principle, I think redux-routine's central idea that "since controls are centrally defined, it requires the conscious decision on the part of a development team to decide when and how new control handlers are added" and that this should lead to "a common domain language for expressing data flows" is very compelling, but things haven't really turned out this way. Maybe this is because it expected developers to be more meticulous in how they could iterate on this common language, or maybe it dismissed the reality that developers don't feel entitled to make these high-level changes; or maybe the reality is just that the application's actions all have very specific control flow needs that no common language can ergonomically support. Still, I would say that this premise was an accurate reflexion of the human structure of Gutenberg back when redux-routine was introduced (July 2018, six months before WordPress 5.0).
  • Nowadays, async/await is a native capability of browsers, and this matters for one big reason: stack traces. If something goes wrong in a nested chain of asynchronously called async functions, I should be able to trace it up to the outermost dispatch. In contrast, any approach based on the command pattern takes control away from the runtime. I don't know about you, but my number one frustration when debugging Gutenberg is the poor traceability of data-level actions and UI-level effects.
  • The testability of effects is pretty nice, though, and a broader adoption of thunks would warrant some discussion on how we'd like to approach testing in general.
  • For me, personally, the benefits of async/await tracing outweigh the shortcomings in testing, but keep in mind that the time that I devote to coding nowadays is short.

The only thing I would maybe add is an actual snippet from a purely-functional language, be it Haskell

I can try that, although I never really wrote even a single line of Haskell or Elm, not even a Hello World 🙂 My knowledge has been purely theoretical until now. Let's see what I can do.

I gave this a go while my basic Haskell knowledge is relatively fresh. I cheated, in that I just wanted to simulate a redux action with a snippet that nevertheless compiles:

-- Pretend that we have defined an Effect monad. For the sake of quickness, I'm
-- just relying on IO. The State monad could also work.
type Effect = IO Bool

-- A dummy effect that mimics state selection. Always returns false.
isFeatureActive :: String -> Effect
isFeatureActive name = return False

-- A dummy effect that mimics dispatch. Has no real effect.
setFeature :: String -> Bool -> Effect
setFeature name = return

-- The actual effects-powered action. It is entirely analogous to the JS
-- generators.
toggleFeature :: String -> Effect
toggleFeature featureName = do
        featureState <- isFeatureActive featureName
        setFeature featureName (not featureState)

main = do
        result <- toggleFeature "foo"
        putStrLn $ "Feature foo is now " ++ (if result then "active" else "inactive")

@jsnajdr
Copy link

jsnajdr commented Oct 11, 2021

I cheated, in that I just wanted to simulate a redux action with a snippet that nevertheless compiles:

That's awesome, thanks @mcsf for the example! I only have one thing to add: how to write the program without and with the do notation. Another way to write the toggleFeature and main functions is:

toggleFeature featureName = isFeatureActive featureName >>= (\featureState -> setFeature featureName (not featureState))
main = toggleFeature "foo" >>= (\result -> putStrLn ("Feature foo is now " ++ (if result then "active" else "inactive")))

Here the >>= operator, called bind, is something similar to a JS .then. And the (\x -> ...) syntax is a lambda function. In JavaScript, this syntax would be equivalent to:

toggleFeature = ( featureName ) => isFeatureActive( featureName ).then( featureState => setFeature( featureName, ... ) );
main = () => toggleFeature( 'foo' ).then( result => putStrLn( 'Feature foo is now ' + ... ) );

To prevent too much chaining and nesting, Haskell has syntax sugar for the >>= ( \x -> ... ) syntax. Similar to async/await:

toggleFeature = async ( featureName ) => {
  const featureState = await isFeatureActive( featureName );
  await setFeature( featureName, ! featureState );
};

main = async () => {
  const result = await toggleFeature( 'foo' );
  await putStrLn( 'Feature foo is now' + result ? ... : ... );
};

If you look at both Haskell and JavaScript examples, you should recognize that the structure of the code is the same.

The main "functional" point of the Haskell code is that the main function builds some structure using the toggleFeature, putStrLn and >>= functions and returns it. The structure has IO type. That kind of terminates the program and now the runtime is supposed to interpret the return value of main somehow. It does interpret it by executing an effect and then kind of "restarting" the program by calling the (\featureState -> ...) callback with the result of the effect, featureState. That's very similar to JavaScript event loop, where you might execute a fetch( '/foo' ).then( ... ) statement, giving control back to the runtime, and then one day the runtime will call the .then( ... ) callback again.

@jsnajdr
Copy link

jsnajdr commented Oct 11, 2021

In principle, I think redux-routine's central idea that "since controls are centrally defined, it requires the conscious decision on the part of a development team to decide when and how new control handlers are added"

I think that another consequence of centrally defining controls and side effects is that they are treated as something mysterious and dangerous that needs to be contained at one place and carefully isolated from everything else. Like some nuclear reactor. But a REST API call, like apiFetch( '/foo' ), doesn't really deserve this status. It's not that dangerous.

One undesirable outcome of treating REST API requests this way is that developers who become convinced that they are dangerous are willing to write 300+ lines and 4 files of Redux code just to fetch a trivial bit of information from REST API. They often don't question the assumption because, like, when you're working with nuclear fuel then surely you need a lot of expensive equipment and huge overhead to do that, right?

@jsnajdr
Copy link

jsnajdr commented Oct 11, 2021

The testability of effects is pretty nice, though, and a broader adoption of thunks would warrant some discussion on how we'd like to approach testing in general.

Watching the canonical Effects as Data talk last week, I found out there are two ways how to test functional generators with effect runtimes.

The first way is what we do in Gutenberg: run the generator and test that the effect values it yields are the expected ones. We're never executing the effects in any way. That's a way of testing that is focused on double-checking the implementation of the body of the generator, and doesn't test observable behavior at all.

The second way, the one that the speaker in that talk recommends, is to run the generator against a mocked effect runtime. You define a runtime:

const mockRuntime = ( effect ) => {
  switch ( effect.type ) {
    case FETCH:
      return mockFetch();
    case DISPATCH:
      return mockDispatch();
  }
}

then execute the generator with that runtime, and then check that it had the expected outcome regarding the mocked effects.

This second way actually tests behavior (if the mocks are reasonable), and the benefit is that there is only one mock and that it's centralized: there is no mocking of a random set of window.fetch and other DOM APIs.

@jsnajdr
Copy link

jsnajdr commented Oct 11, 2021

Looking at the very first code snippet:

store.dispatch( ( { select, dispatch } ) => {
    return "I'm a thunk! I can use selectors and dispatch actions.";
} );

you can only do this (dispatch a function or select with a function) inside a thunk, but not with the regular registry API:

registry.dispatch( 'foo' )( ( { select, dispatch } ) => {
    return "I'm a thunk! I can use selectors and dispatch actions.";
} ) );

This won't work because dispatch( 'foo' ) is an object with action methods, not a callable function.

The reason is that using anything else than a public selector or action is private API, to be used only inside the store to implement its internals, but not to be used publicly.

On the one hand it can be confusing -- select and dispatch works differently at different places -- but on the other hand, the public/private distinction makes sense.

Other than that, I like the updated tutorial a lot!

@adamziel
Copy link
Author

Great catch @jsnajdr! I removed the dispatch bit and only left the minimal thunk definition to give a glimpse of what the rest of the post is about.

@adamziel
Copy link
Author

I don't know about you, but my number one frustration when debugging Gutenberg is the poor traceability of data-level actions and UI-level effects.

@mcsf I share the same frustration, I struggled with grasping how core-data works because all the stack traces leads 🕵️ ran dry around rungen. Fortunately, the use async/await makes the stack traces actually useful.

Here the >>= operator, called bind, is something similar to a JS .then. And the (\x -> ...) syntax is a lambda function. In JavaScript, this syntax would be equivalent to:

@jsnajdr ooh I really like the .then / bind parallel. Promise are pretty close to being monads, so using them makes talking about bind easily-digestible. 👍

@jsnajdr
Copy link

jsnajdr commented Oct 12, 2021

Promise are pretty close to being monads

Thanks for that link! Looking at the counterexamples, I would say that JS promises are monads for all practical purposes. Unless you have objects with a then property, you'll never see the laws being broken.

@gziolo
Copy link

gziolo commented Oct 24, 2021

Thank you for working on this tutorial. It’s very important to make it visible in the Block Editor Handbook once the new API becomes stable. I guess it always remains complex to reason about use cases where a simple action is not enough. While the idea of controls seems great, in practice it isn’t always so straightforward where to draw a line for a side-effect. I run into that recently when working on changes to the block registration in WordPress/gutenberg#34299. The interesting observation is that I started with controls but finally landed on the solution almost identical to what thunks offer based on the feedback from review. The most important part here is also that controls are very difficult to use. You need to split code between two or more places, and be able to reason about complex data flow that is enforced by that. All that gets simplified with thunks only at the cost of using a magic function syntax that exposes the registry 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment