AMA Custom Blocks Contest Edition #3 - Releasing your block

When invoking a custom block from a button, is there a way to tell when a button has been pressed again? I can tell if a button for a different record has been pressed because I can compare the current record with the previous record, but I would also like to be able to tell if the button for the same record has been pressed again.

Use case: the user presses the button to start a wizard, which progresses through some screens. If the user presses the button again, I would like to start the wizard over again from the beginning.

I think what Kasra was getting at is closer to your latter point – writing unit tests against stateless logic which doesn’t interact directly with the base (e.g. testing data manipulation logic that happens before sending to the base or after fetching from the base). You could use mock data and/or stubs to help seed the functions you’re testing.

Improving the testability of blocks & coming up with a more concrete guide of best practices is something we’re thinking about for the future!

Unfortunately that isn’t possible; blocks operate in an iframe with a separate context from the rest of the page. We’re thinking about other ways to provide notifications (e.g. toasts), but that isn’t something that currently exists.

Jeremy’s answer should solve this – if you store the newly created record ID in some local state, then on a future re-render the records returned from useRecords should contain the new record, and you can call expandRecord.

If you did want to fetch the new record in the same block of code, I think you could do so doing something like this:

const newRecordId = await table.createRecordAsync(...)
const freshQueryResult = await table.selectRecordsAsync()
expandRecord(freshQueryResult.getRecordById(newRecordId))

But depending on how often this gets called, it may be better to just wait for the next render cycle.

1 Like

What determines how soon a new render occurs?

How do you recommend showing a <Loader scale={0.3} /> while doing a fetch?
Do I use state to keep track of when the fetch has been sent, and then change state when the response is received? Is there an easier way?

That information is set automatically, by the person who last runs block release . Like you noted, it’s tied to an Airtable account so you can’t manually edit it (but you can have a creator on your base release a version of the block to change it to their name and email).

That version isn’t exposed anywhere, but can be used internally (e.g. to supplement your commit history). When you block release, all installations of that block will reference the new code, so it isn’t possible for users to be on different versions of the same block currently. We’re thinking about ways to expose when a new release has been pushed up to the consumer!

Under the hood, versioning is still something that can be done - for example if you there are required schema changes you’d like to enforce upon a new release. One strategy you could take here is to store it version in GlobalConfig, then perform some logic that runs in your root component and can perform necessary operations if needed –

e.g.

if (globalConfig.get('blockVersion') < 2) {
    globalConfig.setPathsAsync([
        {path: 'someNewPath', value: 'default'},
        {path: 'blockVersion', value: 2}
    ])
}

This could be wrapped into a custom useBlockVersion hook or similar.

1 Like

It largely depends on how the block is architected, but here are some examples –

If you’re using a standard react useState hook, a rerender will trigger anytime you modify the state using the returned setter (e.g. const [value, setValue] = useState(0) whenever setValue is called).

If you’re using the useRecords hook, a rerender will fire anytime a record is deleted / added in the base, or cell values change, or record colors change (this includes changes that propagate from the block itself). For the hooks provided by the SDK, this is determined by the watchable keys. This is a little in the weeds, but you can see which watchable keys useRecords uses here. You can even define your own triggers by using useWatchable on an Airtable model directly

1 Like

Hmm, I’m not sure what might be causing this off the top of my head. This is on block init ? If this continues occurring or you’re able to reproduce more reliably, please write in to blocks@airtable.com so we can try to debug further

Adding your .block folder to your git ignore will prevent it from syncing to your git remote, but it’ll still be on your local computer. If you’re the only one working on the custom block and you only use one computer, this isn’t an issue (otherwise, everyone else collaborating on your block will need the same app and base IDs). If you’re concerned about other users, ignoring your .block folder should be fine! They can choose “Remix from GitHub” when creating a custom block, and it will automatically create a .block folder for them.

Ah, interesting use case. With the useRecordActionData hook, the data always will be the latest press, so you wouldn’t register a separate click. I think you can register a callback directly, though, using registerRecordActionDataCallback which should execute the registered callback on every push (even if its the same button).

I wonder if there’s value in providing a timestamp or ID in the action payload to distinguish between these clicks using the hook.

EDIT: Another solution here would be to use a usePrevious hook, e.g.

function usePrevious(value) {
	const ref = React.useRef();
	React.useEffect(() => {
		ref.current = value;
	});
	return ref.current;
}

This way, you can compare the value returned by useRecordActionData() for subsequent presses. Even if the same record is clicked, the object returned by useRecordActionData on a subsequent button press will be a new object compared to the value cached by usePrevious.

function MyComponent() {
    const buttonData = useRecordActionData();
    const lastButtonData = usePrevious(buttonData);
    useEffect(() => {
        if (buttonData !== lastButtonData && buttonData) {
            // perform logic for button press
        }
    }, [buttonData, lastButtonData])
    
   // ... rest of component ...
}
1 Like

I think that’s the most logical way – barebones example:

function MyComponent() {
    const [isLoading, setIsLoading] = useState(false);
    
    function fetchTheThing() {
        setIsLoading(true);
        
        fetch('example.com').then(response => {
            // do stuff with response
            setIsLoading(false);
        })
    }

    return (
        <Box>
            {isLoading ? (
                <Loader />
            ) : (
                <RestOfApp />
            )}
        </Box>
    )
}
1 Like

Correct, currently only creators can create, release, and install blocks in a given base. It’s not currently possible to install a custom block without creator permissions. If the developer/creator is removed from the base, the block will remain. Releasing, publishing, and sharing blocks is something we’re thinking about for the future in order to reduce friction for both the developer and consumers.

2 Likes

And that’s a wrap! If you have any further questions, you can always find us in the custom blocks beta forum. The winners for this week are @Yuriy_Klyuch, @kuovonne, and @Kirill_Madorin!

With the contest deadline fast approaching (July 6th), we’ll be hosting our last AMA in the custom blocks contest series next week with a twist! Instead of AMA, we’ve got a fun “AYA” format planned. The post will be up EoD Friday and will be open until 6pm PT on Thursday, July 1. We’ll be giving out Amazon gift cards to three of the most insightful posts. Stay tuned!

Exciting to see your block in action! I’ll have to refrain from providing feedback now, assuming this is an entrant in the current blocks contest. Happy to answer your questions, though!

  1. It’s not currently possible to specify field values using RecordQueryResultOpts, but if you create a view in the main product that performs this filtering, you can select records from just that view.
  2. Unfortunately, linked record fields are not included in the current scope of metadata writes (along with formulaic fields) - some work still needs to be done on what that API looks like.
  3. You can’t currently configure the build, but CSS can be loaded using loadCSSFromString or loadCSSFromURLAsync.
  4. We don’t currently provide any built-in tooling for error logging. In theory, you could log errors to an “Errors” table in the base, but that may not be the best approach depending on your use-case.
1 Like