Skip to main content

4 posts tagged with "how to"

View All Tags

Calling Mandatory Getter props

· 12 min read

hilly landscape in the village of simon, brasov Hilly Landscape in the Village of Şimon.

...there's a lot going on with the attributes and the handlers and the ref and the other properties...

Let's assume that we have the following scenario. We have a custom hook that returns getter props, and we want to make sure that those getter props are called correctly. But, first things first:

What is a getter prop?

It's a function that gets returned from the custom hook, which is supposed to be called and its result used for some reason. Let's take the getInputProps getter prop from downshift:

import * as React from 'react'
import {useCombobox} from 'downshift'

const items = ['red', 'green', 'blue']

const {getInputProps} = useCombobox({
items,
})

return (
<div>
<input {...getInputProps()} />
</div>
)

The getInputProps result is an object with properties such as: aria-labelledby, aria-expanded, role, onChange, onClick etc. These properties are going to make our input more or less acting like a combobox. The combobox logic is kept inside the custom hook, the inputs are retrieved through the event handler functions (onClick, onChange) and the combobox related information is passed through the other attributes (aria-labelledby, aria-expanded). Of course, there are many more handlers and properties, but we are keeping it simple for now.

There's also the ref property that is going to be returned from getInputProps, and this is needed in order to allow useCombobox to access the input element and perform some custom .focus() actions when needed.

The Problem

Given that there's a lot going on with the attributes and the handlers and the ref and the other properties, we want to make sure that the getInputProps getter function is called correctly.

  • First of all, we want to make sure that it is called.
  • Secondly, that is called on an <input> element, or in such a way that it has access to an <input> element.
  • Thirdly, in case it is not called correctly, we want to inform the user through a console error and give them enough information to fix the getter function call.
  • Moreover, we also want to show the console error in the development environment, not production. -In addition, we would like to show this error only on the first render, as we believe it's enough in terms of checks and informations.
  • Last, but not least, we will also provide an escape hatch to avoid getting this error if the consumer just cannot call getInputProps properly on the first render.
    • We are going to document this escape hatch and make sure the consumer knows why we are adding it. Ideally, if a consumer will use the escape hatch, they are aware how getInputProps works, how it should be called, and if they, for some reason, cannot call it on first render (using a Portal for example), they will use the escape hatch to avoid the error, but they will test their implementation to make sure that it works as it would by calling getInputProps normally.

The Solution

Inside the getInputProps function we return, we would like to have a mechanism that checks whether the function is called withing the right conditions. We can do so with a function to which we supply the call parameters. This function will be called at render time, since getInputProps is called on render time. The actual check happens after the first render, so we will involve a React.useEffect hook. After a bit of thinking, the solution is actually simple: a custom React hook that will keep call information inside a React ref. This call information will be supplied by a function which we will return from the hook. The returned function will store the call information inside that ref. And inside the effect function, we will perform the check and show the errors if needed.

The Tests

We actually have a good and simple solution in mind, so let's see what do we actually expect from this solution.

  • we want an error displayed when getInputProps is not called.
  • we want an error displayed if getInputProps is not called with a ref pointing to an element.
  • we don't want an error displayed if getInputProps is not called on subsequent renders.
  • we don't want an error displayed if getInputProps is called correctly.
  • we don't want an error displayed if we use the escape hatch provided and getInputProps is not called with a ref pointing to an element.

We will use React Testing Library renderHook and together with Jest, we will set up our tests suite.

describe('non production errors', () => {
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {})
})

afterAll(() => {
jest.restoreAllMocks()
})

test('will be displayed if getInputProps is not called', () => {
renderHook(() => {
// we just render the hook, we don't call getInputProps.
useCombobox({items})
})

expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(
`downshift: You forgot to call the getInputProps getter function on your component / element.`,
)
})

test('will be displayed if element ref is not set and suppressRefError is false', () => {
renderHook(() => {
const {getInputProps} = useCombobox({
items,
})

getInputProps()
})

expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(
`downshift: The ref prop "ref" from getInputProps was not applied correctly on your element.`,
)
})

test('will not be displayed if getInputProps is not called on subsequent renders', () => {
let firstRender = true
const {rerender} = renderHook(() => {
const {getInputProps} = useCombobox({
items,
})

if (firstRender) {
firstRender = false
getInputProps({}, {suppressRefError: true})
}
})

rerender()

expect(console.error).not.toHaveBeenCalled()
})

test('will not be displayed if called with a correct ref', () => {
// we supply a mock ref function to getInputProps.
const refFn = jest.fn()
const inputNode = {}

renderHook(() => {
const {getInputProps} = useCombobox({
items,
})

// getInputProps returns a ref function which will make the element
// usable both outside and inside useCombobox.
const {ref} = getInputProps({
ref: refFn,
})

// we call the final ref function received with a dummy node.
ref(inputNode)
})

expect(console.error).not.toHaveBeenCalled()
})

test('will not be displayed if element ref is not set and suppressRefError is true', () => {
renderHook(() => {
const {getInputProps} = useCombobox({
items,
})

getInputProps({}, {suppressRefError: true})
})

expect(console.error).not.toHaveBeenCalled()
})
})

The tests are quite self explanatory given our 5 expected use cases. For now, they will obviously fail, and we will consider our job done once we write the implementation and the tests are green.

The Implementation

To recap, the getter prop is called on render, so we would like our check to be performed after the render. We will store the call information inside a React ref, and we will check it using a React useEffect. Since we need our check to be performed in development mode, not production, we will define the custom hook like this:

let useGetterPropsCalledChecker = () => {}

if (process.env.NODE_ENV !== 'production') {
useGetterPropsCalledChecker = () => {
// the actual implementation.
}
}

First and foremost, we would like to set up our ref object that we will use for the check.

if (process.env.NODE_ENV !== 'production') {
useGetterPropsCalledChecker = (...propKeys) => {
const getterPropsCalledRef = useRef(
propKeys.reduce((acc, propKey) => {
acc[propKey] = {}

return acc
}, {}),
)
}
}

And we will call this hook inside useCombobox like this:

useGetterPropsCalledChecker(
'getInputProps',
'getToggleButtonProps',
'getMenuProps',
)

Notice that we will most definitely want the hook to check that other getter props are called correctly as well. We are going to stick to getInputProps here purely for explanatory purposes. Anyway, for each prop key we pass to the hook, we will create an empty object container inside the ref. We add this empty object as we want, during our checking phase, to be able to tell if the getter prop with that prop name was not called. We cannot perform this check if we do not store any initial information about the getter props. What prop keys should we actually check, right?

Next, we want to give the consumer, which is the getter prop function, to be able to store the call information. For that, we will return a function from the hook, which stores this information within the ref.

if (process.env.NODE_ENV !== 'production') {
useGetterPropsCalledChecker = (...propKeys) => {
const getterPropsCalledRef = useRef(
propKeys.reduce((acc, propKey) => {
acc[propKey] = {}

return acc
}, {}),
)
}

const setGetterPropCallInfo = useCallback(
(propKey, suppressRefError, refKey, elementRef) => {
getterPropsCalledRef.current[propKey] = {
suppressRefError,
refKey,
elementRef,
}
},
[],
)

return setGetterPropCallInfo
}

And the usage within getInputProps is going to be something like this:

// this is going to end up on the input element via getInputProps
const inputRef = React.useRef(null)

const getInputProps = useCallback(
({refKey = 'ref', ref, ...rest} = {}, {suppressRefError = false} = {}) => {
setGetterPropCallInfo('getInputProps', suppressRefError, refKey, inputRef)

// rest of the logic

return {
[refKey]: mergeRefsUtil(ref, inputNode => {
inputRef.current = inputNode
}),
// the rest of the attributes and handlers that will end on the input
}
},
)

In case the above ref logic is not clear, it ensures that all ref objects have access to the same input element. We need the input ref in useCombobox. The consumer might also need the input ref for some additional logic, hence the merge of these ref objects and passing the merged object to the <input> element. Also, suppressRefError is the escape hatch we talked about previously.

Now the checking part. We have the initial object created for each getter prop on render phase, when useGetterPropsCalledChecker is called. We have the call information stored when getInputProps is called, also on render phase. Now we have to check if the function is called correctly, on first render only, so we add a React useEffect hook inside our custom hook:

useEffect(() => {
Object.keys(getterPropsCalledRef.current).forEach(propKey => {
const propCallInfo = getterPropsCalledRef.current[propKey]

if (!Object.keys(propCallInfo).length) {
// eslint-disable-next-line no-console
console.error(
`downshift: You forgot to call the ${propKey} getter function on your component / element.`,
)
return
}
})
})

Our first test should pass. If the object is empty, then it means that we did not call the getter prop at all, so we show an error. If the getter prop is actually called, then we need to check if the element exists, which means that the getter prop is called correctly on an element.

useEffect(() => {
Object.keys(getterPropsCalledRef.current).forEach(propKey => {
const propCallInfo = getterPropsCalledRef.current[propKey]

if (!Object.keys(propCallInfo).length) {
// eslint-disable-next-line no-console
console.error(
`downshift: You forgot to call the ${propKey} getter function on your component / element.`,
)
return
}

const {refKey, elementRef} = propCallInfo

if (!elementRef?.current) {
// eslint-disable-next-line no-console
console.error(
`downshift: The ref prop "${refKey}" from ${propKey} was not applied correctly on your element.`,
)
}
})
})

Our second test should now pass. The element check is, sure, not ideal, since we're not checking for an actual HTML input element, but let's say it suffices for now. The fourth test which checks that there is no error logged was passing already, but now with the actual element check, we make sure that, when it's passing, it does so for the right reason.

To fix the second test, we need our hook to be called only once, on first render, so we add [] as the second parameter to useEffect.

And to fix the fifth test, which checks the escape hatch, we will also involve the suppressRefError parameter from the call information. Our final implementation will look like this.

if (process.env.NODE_ENV !== 'production') {
useGetterPropsCalledChecker = (...propKeys) => {
const getterPropsCalledRef = useRef(
propKeys.reduce((acc, propKey) => {
acc[propKey] = {}

return acc
}, {}),
)

useEffect(() => {
Object.keys(getterPropsCalledRef.current).forEach(propKey => {
const propCallInfo = getterPropsCalledRef.current[propKey]

if (!Object.keys(propCallInfo).length) {
// eslint-disable-next-line no-console
console.error(
`downshift: You forgot to call the ${propKey} getter function on your component / element.`,
)
return
}

const {suppressRefError, refKey, elementRef} = propCallInfo

if (suppressRefError) {
return
}

if (!elementRef?.current) {
// eslint-disable-next-line no-console
console.error(
`downshift: The ref prop "${refKey}" from ${propKey} was not applied correctly on your element.`,
)
}
})
}, [])

const setGetterPropCallInfo = useCallback(
(propKey, suppressRefError, refKey, elementRef) => {
getterPropsCalledRef.current[propKey] = {
suppressRefError,
refKey,
elementRef,
}
},
[],
)

return setGetterPropCallInfo
}
}

Recap

Basically, we initialize an empty object for each getter prop we want to check, using the getterPropsCalledRef React ref object. Then we return the setGetterPropCallInfo function that will store information for each getter function when called. Finally, inside the useEffect function, for each getter prop, we get the call info from the ref, we check for an empty object and show the getter function not getting called error. In case we our object in not empty, we get the call information, and do nothing if suppressRefError is true. Otherwise, we check the actual element ref and show an error if it's falsy.

We may go even further with the tests and check if the hook is actually an empty object on production environments, but at this point I'm not sure if it's possible to implement such a test, or if it's even worth it. Bottom line is that, using this custom hook, we are able to perform the function getting called check, or other checks as well, in order to make sure our consumers are doing the right thing and the solution we ship works for them.

TDD a Downshift feature request

· 12 min read

praia da arrifana, portugal Praia da Arrifana, Portugal.

I don't always do TDD, but when I do, I write about it.

From the GitHub issue to the next library update on npm, we are going to push a feature request via the magic of Test Driven Development. It's easier than you think, and next time when the interviewer asks if you've heard about TDD, you will smile, say yes, then test drive their Leetcode puzzle and bring home the biggest offer they could make. TDD is also great in day-to-day work when adding features for your products, fixing issues and solving open source tickets, which is what we're going to do here.

The Problem

Let's take the following small feature request from Downshift:

Select element doesn't receive focus when the label is clicked.

Test driving this feature means that we need to write the tests before the feature. And that's a good thing, because writing the tests first will make us ask the most relevant question, what exactly do I want to achieve here.

That usually means drafting a list of requirements, and based on that list of requirements, we will write the tests. Running the tests will result in test failures (at least they should) and by implementing the feature the tests will start turning green. Once all the tests are green, it means our feature is complete. This is called red-green refactoring.

Before making the list of requirements, let's also visualise how this select element is built with Downshift's useSelect.

const {getToggleButtonProps, getLabelProps, getMenuProps, isOpen, ...rest} =
useSelect({
items,
})

return (
<div>
<label {...getLabelProps()}>Books:</label>
<div {...getToggleButtonProps()}></div>
<ul {...getMenuProps()}>
{isOpen ? items.forEach(/* render books if open */) : null}
</ul>
</div>
)

This will build a markup similar to:

<div>
<label id="label-id-1" for="toggle-id-1">Books:</label>
<div
role="combobox"
aria-expanded="false"
tabindex="0"
aria-controls="menu-id-1"
aria-haspopup="listbox"
aria-activedescendant
aria-labelledby="label-id-1"
></div>
<ul role="listbox" id="menu-id-1" aria-labelledby="label-id-1">
</div>

The Requirements

  1. Clicking the <label> should place the focus on the combobox element.

This would have been done automatically if the labelled element would have been an <input> or a <button>, given how the label's for attribute works by default. However, if the labelled element, here the combobox, is a <div> (ARIA 1.2 spec), the focus must be added manually via a click event. We would have the same issue if we replace the <label> with something else, so another reason to add this behaviour.

  1. Passing an onClick to getLabelProps should not interfere with the default focus behaviour we will be adding.

For instance, calling the code below should focus the toggle button and display the log.

<label
{...getLabelProps({
onClick() {
console.log('Clicked!')
},
})}
/>
  1. Passing an onClick to getLabelProps adds the preventDownshiftDefault attribute on the click event object should not focus the toggle anymore.

It's a practice similar to the event's preventDefault, where we can control the default behaviour of the event. In Downshift's case, we want to be able to stop the Downshift default behaviour.

<label
{...getLabelProps({
onClick(e) {
e.preventDownshiftDefault = true

document.querySelector('#better-element').focus()
},
})}
/>

The Tests

That's the beauty of drafting a list of requirements, you could use it to write the tests. Downshift is a React library, so we are going to use React Testing Library and Jest in order to check our changes will be correct.

Our first test is going to be easier, since we only want to render the Select, click on the label, and check if the toggle has focus. To render the component, we will use RTL render method with a JSX similar to the example when describing the problem.

function renderSelect() {
const utils = render(<Select />)

return utils
}

function Select() {
const {getToggleButtonProps, getLabelProps, getMenuProps, isOpen, ...rest} =
useSelect({
items,
})

return (
<div>
<label {...getLabelProps()}>Books:</label>
<div {...getToggleButtonProps()}></div>
<ul {...getMenuProps()}>
{isOpen ? items.forEach(/* render books if open */) : null}
</ul>
</div>
)
}

With these abstractions in place, our test becomes:

test('clicking the label moves focus on the toggle element', async () => {
renderSelect()

// RTL's user-event tool
await user.click(screen.getByText('Books:', {selector: 'label'}))

// RTL jest-dom extends Jest's expectations to include "toHaveFocus".
expect(screen.getByRole('combobox', {name: 'Books:'})).toHaveFocus()
})

And we're done. We render, we click the label, we check if the focus is where we need it to be. We run the test, it will fail, it's a success, we go grab lunch.

Now, something about the other 2 requirements. These are part of Downshift's API for some time now, and they already have a utility function that chains callbacks together and calls all of them on a event trigger. This utility also knows not to call Downshift's default event handler if "preventDownshiftDefault" has been passed. We are not going to test this utility now, we just want to make sure it works for our callback. Sure, we can just mock that utility and check the call, since we also test the utility in isolation to make sure that it works. But it's better to test for the actual user consequences rather than implementation details, so let's write the tests for these last requirements.

For the second requirement, if we pass our own onClick to getLabelProps, it should get called along with the default handler that sets focus. For this particular test, we don't need to render the whole component, we can just render the hook, like this.

export function renderUseSelect(props) {
// renderHook from RTL
return renderHook(() => useSelect({items, ...props}))
}

Now, we need to find a way to check that our custom onClick is called when the label gets clicked. Which, in turn, means that we need to actually trigger that click, since we are not going to render actual markup, we are just testing the hook. We know that getLabelProps will return an onClick that should end on a label element, so all we have to do is to pass a Jest spy as an onClick argument to getLabelProps, then call the onClick returned, and check the spy.

test('event handler onClick is called along with downshift handler', () => {
const userOnClick = jest.fn()
const {result} = renderUseSelect()

const {onClick} = result.current.getLabelProps({
onClick: userOnClick,
})

onClick()

expect(userOnClick).toHaveBeenCalledTimes(1)
})

Simple! Now we test that whatever we pass in the onClick, it will be called with Downshift's handler. Which is something we don't test at the moment, and that's a huge potential problem over there. We don't want a regression to slip in one day and ruin everyone's Select elements if they also wanted to pass their own event handlers to our prop getters, which is a very common use case.

This is the part of the TDD where we need to think about the implementation. To manually focus the toggle element, we need to use a React ref on the combobox toggle JSX element, and use that ref object to perform a manual .focus(). Consequently, there's going to be a ref object returned by getToggleButtonProps, as it needs to be passed to the toggle element. We will use the ref in the useSelect code in order to perform the focus action. In our test, we just need to grab this returned ref and call it with a mocked object that has a focus function attached to it, which will be a jest spy. If our implementation works, the object we pass as argument to the ref function call is going to be used by the internal code to perform the focus. The test becomes:

test('event handler onClick is called along with downshift handler', () => {
const userOnClick = jest.fn()
const mockToggleElement = {focus: jest.fn()}
const {result} = renderUseSelect()

const {onClick} = result.current.getLabelProps({
onClick: userOnClick,
})
const {ref} = result.current.getToggleButtonProps()

ref(mockToggleElement) // simulate that React will add the toggle element in the ref.
onClick() // now we should have the ref containing the toggle element, we can click the label.

expect(userOnClick).toHaveBeenCalledTimes(1)
expect(mockToggleElement.focus).toHaveBeenCalledTimes(1)
})

We also need to check that setting "preventDownshiftDefault" will not focus the toggle element. The test is going to be similar to the previous one, only that the userOnClick implementation will set this attribute on the event object.

test('event handler onClick is called along with downshift handler', () => {
const userOnClick = jest.fn(event => {
event.preventDownshiftDefault = true
})
const mockToggleElement = {focus: jest.fn()}
const {result} = renderUseSelect()

const {onClick} = result.current.getLabelProps({
onClick: userOnClick,
})
const {ref} = result.current.getToggleButtonProps()

ref(mockToggleElement) // simulate that React will add the toggle element in the ref.
onClick() // now we should have the ref containing the toggle element, we can click the label.

expect(userOnClick).toHaveBeenCalledTimes(1)
expect(mockToggleElement.focus).not.toHaveBeenCalled()
})

We are in a very good spot right now. We have written great tests that communicate exactly what our goal is, and that will only smoothen the implementation process. While deciding on the requirements and writing the tests we can also get better feedback from all stakeholders, since we already have a list of objectives, and it's easier to collaborate on that. We could get extra requirements (more tests) or, even better, some may decide that some requirements are superfluous. So what we may need to remove some already written tests? Less code is good.

Implementation

The implementation is rather easy. Given the current code, we will add another onClick function to be returned from getLabelProps, and we will use the toggleButtonRef we already have in order to focus the element.

Before:

const getLabelProps = useCallback(
labelProps => ({
id: elementIds.labelId,
htmlFor: elementIds.toggleButtonId,
...labelProps,
}),
, [elementIds]
)

After:

const getLabelProps = useCallback(
({onClick, ...labelProps} = {}) => {
const labelHandleClick = () => {
toggleButtonRef.current?.focus()
}

return {
id: elementIds.labelId,
htmlFor: elementIds.toggleButtonId,
onClick,
...labelProps,
}
},
[elementIds],
)

Success! One test is passing already, with 2 more to go. Once these are green, we can agree that our implementation is a success. And that's the easiest part. We will also use our callAllEventHandlers utility in order to chain the callbacks and also check for the "preventDownshiftDefault" property.

const getLabelProps = useCallback(
({onClick, ...labelProps} = {}) => {
const labelHandleClick = () => {
toggleButtonRef.current?.focus()
}

return {
id: elementIds.labelId,
htmlFor: elementIds.toggleButtonId,
onClick: callAllEventHandlers(onClick, labelHandleClick),
...labelProps,
}
},
[elementIds],
)

Utility Function for Reference

The callAllEventHandlers utility will call the functions passed to it, in order, until one of them passes the "preventDownshiftDefault". It's important to pass the focus setting handler at the end. If you need such a function in your life, here it is:

/**
* This is intended to be used to compose event handlers.
* They are executed in order until one of them sets
* `event.preventDownshiftDefault = true`.
* @param {...Function} fns the event handler functions
* @return {Function} the event handler to add to an element
*/
function callAllEventHandlers(...fns) {
return (event, ...args) =>
fns.some(fn => {
if (fn) {
fn(event, ...args)
}
return (
event.preventDownshiftDefault ||
(event.hasOwnProperty('nativeEvent') &&
event.nativeEvent.preventDownshiftDefault)
)
})
}

You also need to properly compose the ref objects. In our case, the toggle element is going to be used by useSelect, and it may also be used in the component using useSelect. Also for reference, here's the ref composing function and its usage.

const toggleProps = {
// refKey could be ref, could be innerRef, up to the user
[refKey]: handleRefs(ref, toggleButtonNode => {
toggleButtonRef.current = toggleButtonNode
}),
// ... the rest of the props returned by getToggleButtonProps
}

function handleRefs(...refs) {
return node => {
refs.forEach(ref => {
if (typeof ref === 'function') {
ref(node)
} else if (ref) {
ref.current = node
}
})
}
}

Last Changes and Merging

We can create a new branch and commit our new changes. Given the nature of Downshift, we have TS files separately, and sometimes they also need to be updated. Now it's such a case, given that we changed the type of a return value:

export interface UseSelectGetLabelPropsReturnValue
extends GetLabelPropsReturnValue {
onClick: React.MouseEventHandler
}

We also need to update the documentation, to make it clear to our users that we added an event handler to the label props. We also want to mention what the event does.

#### Label

- `Click`: It will move focus to the toggle element.

Finally, we run the validation script, which in Downshift's case, it runs all the unit tests, the coverage check, the e2e tests, linting and all that stuff. Afterwards, we push the changes, create a PR, let the same validation script run via GitHub actions, and if the changes look good, we will merge the PR, close the issue, close the laptop and run. We should get an email from npm that the library has been updated successfully. In our case, it should be a minor version bump, since it's a feature.

Conclusion

I don't always do TDD, but when I do, I write about it. It's a very neat process, and I am super thankful to Kent and his Testing Javascript teachings. I was always passionate about automated testing, but his course just completely changed everything for me. If you haven't done it yet, I strongly recommend it, it's so worth it. Tip: try to get purchase parity for it if you aim to use it in your own home country only. You will get a discount.

I hope my Downshift TDD experience helps you write better and test better in the feature. If it sparked your interest in using TDD more or just to learn to test better, I declare myself happy for it. Good luck in writing great code and improving the world through your work!

Overwriting Property Types in TypeScript

· 14 min read

the library in the morgan museum, new york The Morgan Library Museum in New York. Photo by Silviu Alexandru Avram

TypeScript is a great alternative to JavaScript if you feel the need for that extra safety net of strong typing. Most of the tyme, TS is pretty straightforward, as we are familiar with strong type languages like Java. However, when we do require a tiny bit of dynamic type magic, things can stop being straightforward, and we can end up being very creative. But when we actually get the job done and even improve on the result, we realise how strong TS can be, and we are super thankful for its existence.

In this article, we are going to touch concepts like TypeScript as a language in general, but also Generics and some TS utilities that are available globally. Solving the problem below helped me understand TS better, and I hope it helps you as well. Let's begin.

The Use Case

The JS code below is the one that required some creativity from my part, when improving Typescript types for downshift. For simplicity, I removed some React specific details, since they are not relevant to the topic.

function getToggleButtonProps({
onClick,
onPress,
refKey = 'ref',
ref,
...rest
} = {}) {
function toggleButtonHandleClick() {
dispatch({
type: stateChangeTypes.ToggleButtonClick,
})
}
const toggleButtonProps = {
[refKey]: handleRefs(ref, toggleButtonNode => {
toggleButtonRef.current = toggleButtonNode
}),
'aria-controls': elementIds.menuId,
'aria-expanded': latestState.isOpen,
id: elementIds.toggleButtonId,
tabIndex: -1,
}

if (!rest.disabled) {
if (isReactNative || isReactNativeWeb) {
toggleButtonProps.onPress = callAllEventHandlers(
onPress,
toggleButtonHandleClick,
)
} else {
toggleButtonProps.onClick = callAllEventHandlers(
onPress,
toggleButtonHandleClick,
)
}
}

return {
...toggleButtonProps,
...rest,
}
}

Our function, getToggleProps, receives some props as an Object, which is destructured directly.

  • we are using an empty object as default parameter (= {}), otherwise our destructuring will fail for undefined. Extra safety.
  • we are grabbing a few properties from the object, such as onPress, onClick, ref and refKey, as we are using them in our function.
  • we are using a prop without grabbing it from the object, rest.disabled.
  • we are returning our computed props as toggleButtonProps, along with rest, which are the rest of the props we did not change. We bundled them together by object spreading and returned them.

The Goal

Since this function is written in JS, in order to convert it to TS, or at least provide TS types for it, we need to define the types for both the function parameter and the function return value. The function's purpose is to return attributes that are going to be applied to a HTML button element, but not always. It could be a React Native button as well, or a custom React button component from any UI Library. Consequently, our types requirements are the following:

  1. The parameter should accept HTML button props, in the case of an HTML button, or any custom UI component that also accepts HTMP button props.
  2. The parameter should accept refKey, a string prop used for those rare cases where the component we are passing toggleButtonProps to has a different name for the ref prop, for instance innerRef.
  3. The parameter should also accept an optional onPress, in case we are in the context of React Native.
  4. The parameter should also accept any prop that needs to end up on the element, by being passed through with the rest object.
  5. The return value needs to return the props that are being computed and guaranteed to be returned in toggleButtonProps: aria-controls, aria-expanded, id and tabIndex.
  6. The return value needs to optionally return either onClick or onPress, depending on whether we are on React Native or not.
  7. The return value needs to return those props from #4 that could be anything, but we should also include their type in the return type.
  8. In case there is a type overlap between a prop from the parameter and the one from the return value, the parameter prop type wins.
  9. We cannot do anything for refKey, since that can be any string, with a 'ref' default, so the most we could do here is an optional ref prop.

Implementation

Let's nail the easy static types first, then we will work towards the dynamic override aprt.

HTML Button Props

The parameter should accept HTML button props, in the case of an HTML button, or any custom UI component that also accepts HTMP button props.

For this first part, the implementation is really simple, we just need to make the parameter accept HTMLButtonProps.

function getToggleButtonProps({
onClick,
onPress,
refKey = 'ref',
ref,
...rest
}: React.HTMLProps<HTMLButtonElement> = {}) {
// implementation
}

The generic-based React.HTMLProps<HTMLButtonElement> is a great way to achieve our goal, since it will return all the props that could end up on an HTML button. And all of them are marked as optional.

Very easy so far, but there's one problem. TypeScript will complain about refKey, since the prop is not part of React.HTMLProps<HTMLButtonElement>. And there's also onPress. That's not part of the HTML Button either, since it's a prop specific to React Native. Let's fix that.

The refKey Prop

The parameter should accept refKey, a string prop used for those rare cases where the component we are passing toggleButtonProps to has a different name for the ref prop, for instance innerRef.

In order to fix our code, we can just add the two props with a type intersection, like so:

function getToggleButtonProps({
onClick,
onPress,
refKey = 'ref',
ref,
...rest
}: React.HTMLProps<HTMLButtonElement> & {
refKey?: string
} = {}) {
// implementation
}

Done! Now the props are the intersection between the button props and the ref type we just created, the only thing we need to accept is onPress.

The onPress Prop

The parameter should also accept an optional onPress, in case we are in the context of React Native.

The most obvious fix would be to add onPress to the type we just created to support the refs.

function getToggleButtonProps({
onClick,
onPress,
refKey = 'ref',
ref,
...rest
}: React.HTMLProps<HTMLButtonElement> & {
refKey?: string
onPress?: (event: React.BaseSyntheticEvent) => void
} = {}) {
// implementation
}

For simplicity, we are using React.BaseSyntheticEvent, which is goint to be a common interface for both web and native event handler props in React, since we don't plan to also import React Native types.

The errors are gone and we handled the first 3 use cases, which is great. Since we may want to support other functions as well, not just for toggle buttons, we may want to split the above type into several, which could be used independently. Also, we may want to extend these types, so we should consider using interfaces instead of types. Consequently, our code will become:

interface GetPropsWithRefKey {
refKey?: string
}

interface GetToggleButtonProps
extends React.HTMLProps<HTMLButtonElement>,
GetPropsWithRefKey {
onPress?: (event: React.BaseSyntheticEvent) => void
}

function getToggleButtonProps({
onClick,
onPress,
refKey = 'ref',
ref,
...rest
}: GetToggleButtonProps = {}) {
// implementation
}

Now the refKey part is abstracted away in GetPropsWithRefKey, and can be used by other functions, while the button specific types are bundled together in GetToggleButtonProps, along with the refKey part, through extension.

Pass-through Props

The parameter should also accept any prop that needs to end up on the element, by being passed through with the rest object.

Now comes the hard part. We want to accept anything else the user may need to pass through our function, but still keep our previous GetToggleButtonProps definition for the props. Since we have no idea what the user is going to pass in addition to the types we already defined, we will use a generic type, which we will intersect with GetToggleButtonProps, such that it will include stuff from both. Let's actually write it down:

function getToggleButtonProps<Rest>({
onClick,
onPress,
refKey = 'ref',
ref,
...rest
}: GetToggleButtonProps & Rest = {}) {
// implementation
}

Rest can be anything, and it does not have to be specified directly when calling the function. For example, this call will be considered correct:

function onClick() {
console.log('clicked')
}

const toggleProps = getToggleButtonProps({onClick, foo: 'bar'})

onClick is part of the GetToggleButtonProps interface, while the foo string property is part of the Rest, and, according to the function implementation, it will be passed directly to the return value. The Rest generic will be super valuable when defining the return value type, as we shall see in a moment.

Guaranteed Returned Props

The return value needs to return the props that are being computed and guaranteed to be returned in toggleButtonProps: aria-controls, aria-expanded, id and tabIndex.

Of course, this part is the easy one, and we will create an interface that defines the props above and their types, according to the implementation.

function getToggleButtonProps<Rest>({
onClick,
onPress,
refKey = 'ref',
ref,
...rest
}: GetToggleButtonProps & Rest = {}): {
'aria-controls': string
'aria-expanded': boolean
id: string
tabIndex: -1
} {
// implementation
}

Each property is defined as non-optional, since it is guaranteed that we are returning it in the returned object. We can also be super specific with our types, for instance tabIndex is -1 instead of number, as we are going to always return -1, while the other properties are dynamic.

Optional Event Handler Props

The return value needs to optionally return either onClick or onPress, depending on whether we are on React Native or not.

Let's also add the couple of optional handler props and also abstract the whole return value prop into an interface.

interface GetPropsWithRefKey {
refKey?: string
}

interface GetToggleButtonProps
extends React.HTMLProps<HTMLButtonElement>,
GetPropsWithRefKey {
onPress?: (event: React.BaseSyntheticEvent) => void
}

interface GetToggleButtonReturnValue {
'aria-controls': string
'aria-expanded': boolean
id: string
onPress?: (event: ReactNative.GestureResponderEvent) => void
onClick?: React.MouseEventHandler
tabIndex: -1
}

function getToggleButtonProps<Rest>({
onClick,
onPress,
refKey = 'ref',
ref,
...rest
}: GetToggleButtonProps & Rest = {}): GetToggleButtonReturnValue {
// implementation
}

Looks great! We now have interfaces for both the function parameter and the return value. We are almost at the finish line.

Rest in the Return Value

The return value needs to return those props from #4 that could be anything, but we should also include their type in the return type.

Now it's time to use again the Rest generic we defined previously, and include it in the return value, since it contains the pass-through props. We will use the intersection here, again.

function getToggleButtonProps<Rest>({
onClick,
onPress,
refKey = 'ref',
ref,
...rest
}: GetToggleButtonProps & Rest = {}): GetToggleButtonReturnValue & Rest {
// implementation
}

Now, everything extra that is passed as rest will be reflected in the type for the return value.

TypeScript VSCode snippet reflecting the rest prop being present in the return value

We just crossed the finish line with half a leg.

Prop Overrides

In case there is a type overlap between a prop from the parameter and the one from the return value, the parameter prop type wins.

Suppose that we want to make our toggle button focusable and add it to the tab order of the page. We will need to pass tabIndex as 0, or any other positive value (big anti pattern, but you can). With our current type definitions, if we pass tabIndex: 0 as a prop to the function, the return value type of tabIndex will stay as -1, since that's what we defined in the GetToggleButtonReturnValue interface.

TypeScript VSCode snippet reflecting the tabIndex prop type being -1 though it has the value 0

Sure, we can just make the type to be number and move on, but that's not what we are actually returning by default. And it's not just about the tabIndex. We can have, for instance, another prop returned by default, such as aria-haspopup, with a listbox default value. But what if the user needs to display the popped out menu as a modal? They would need to pass aria-haspopup: 'dialog' to the function, and the return type for aria-haspopup should reflect the parameter type.

For this reason, we need to create a type that overrides the type we define by default for the props that are passed as parameter and have a different type. Of course, this is not required if those props passed as parameter are destructured away from rest, and in this case we can keep our own type, as the value won't be overriden by pass-through. It's not the case for tabIndex, so we need to override the type here. And the type definition I found to solve the issue is the following, from this thread:

type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U

Whoa, whoa, what? Ok, let's try to remember what we want to achieve. If we pass a prop with the same name as the function parameter, then it should override the type from GetToggleButtonReturnValue.

Let's consider GetToggleButtonReturnValue to be T and Rest to be U. Overwriting T with U means picking from T only the prop types that are not common between T and U. We exclude from T the props from U, which means there will be no common props as a result of this exclusion, and then we pick these exact props from T. Finally, we intersect these non-common props with U, giving us back the common props (if any), but this time with the types from U. Remember, U is Rest, our pass-through props type.

Consequently, our return type will become:

type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U

function getToggleButtonProps<Rest>({
onClick,
onPress,
refKey = 'ref',
ref,
...rest
}: GetToggleButtonProps & Rest = {}): Overwrite<
GetToggleButtonReturnValue,
Rest
> {
// implementation
}

And voilà, the result:

TypeScript VSCode snippet reflecting the tabIndex prop type being number in the returned value due to type override

Also important to point out, due to the current type of the parameter GetToggleButtonProps & Rest, you cannot pass just any type to the properties that are statically defined, such as tabIndex, onClick, refKey and the others. Passing a number for the onClick prop will fail since it's clearly defined in the React.HTMLProps<HTMLButtonElement> as a function with certain parameter types. You could pass tabIndex as 0 due tot the fact that it's a number type in the React.HTMLProps<HTMLButtonElement>, even though we made it -1 in our return type, as a result of our function implementation.

The Ref Prop

We cannot do anything for refKey, since that can be any string, with a 'ref' default, so the most we could do here is an optional ref prop.

This is going to be our only limitation. Passing a custom string as a refKey will not reflect a property with the key as that string value in the return type. There is no way (that I know) to get the string value from an object key and use that as a key in a type. Consequently:

TypeScript VSCode snippet reflecting the customInnerRef string value for refKey not ending in the return type

There's no customInnerRef property in the return value, just the ref optional property that is going to be passed by default, in case we don't pass any value for refKey (refKey = 'ref',).

Conclusion

In this post, we went through defining static types for a function's paramater and return value, as well as allowing dynamic types to override previously defined static types, where such a case was needed. We also defined an initial set of expectations, starting from a working example, then we went through the use cases one by one and altered the solution accordingly. We also saw a few usages of TS helper types, such as Pick, Exclude and others.

Improving the Types in Downshift was a great experience, as there were quite a few use cases to consider for the getter prop functions. I found the experience so useful that I considered to write in detail about it, and I hope you will find it useful in some way as well.

Building this Blog

· 26 min read

a heap of logs next to a mountain cabin in the piatra mare mountains, photo by oana vasilescu Photo by Oana Vasilescu

Building a digital blog should not be, theoretically, too hard to accomplish. Expecially now, in 2022, with all the many dedicated platforms and services that have established themselves over time in the digital blogging department.

As someone who does Frontend Development for a living, my list of technical choices is even more generous. I am able, theoretically, to build a web application using just the magic trio of HTML, CSS and JavaScript. Just in case I don't feel like going full vanilla, there are superior levels of abstractions, such as, for example, the ReactJS, tailwindcss and TypeScript combination. Of course, one level of abstraction above, the list continues with the likes of GatsbyJS or NextJS. And just in case I don't want to bother with coding at all, I can go full techless with solutions such as WordPress, MailChimp, Canva, or something equivalent. The last list of services can also provide support for hosting and domain registration, at a cost, obviously.

So, time to choose one technology stack and ... start writing already?

Picking the Tech Stack

The technical solution for this blog is a middle ground between tech skills and convenience. Let's talk about each part.

Github

First and foremost, the source code for the blog needs to live somewhere. Sure, we can keep it safe on a laptop, but what if the device gets damaged, or the code gets accidentally deleted? So much precious information lost, forever. We cannot risk that. It will also be hard to keep track which of the files got updated and need to be published in order for the next blog version to be released. Luckily, we have GitHub to help us with keeping our code safe, versioned and structured for review. The source code for the blog can live happily ever after inside a GitHub code repository. Specifically, this repository.

Docusaurus

The second tech stack choice is docusaurus, which provides us with the infrastructure to build the blog. We can write each blog post as a MDX markdown file that will be transformed automatically into a web page. Docusaurus allows further customisation of the page layout using a technique called swizzling. We can replace or alter different layout components using everyone's favorite library, ReactJS.

All the source code (markdown, react javascript, others) is transformed into production code by docusaurus. This process is called the build process, and its result, the production code, is the blog aplication itself. With great support out of the box for responsiveness, layout, images, tags, code snippets and even interactive code examples, docusaurus does a lot of the heavy lifting and lets me focus on the content.

Netlify

Great, now we can write my blog source code, save it somewhere safe and build the blog from it. Anything else?

Well, turns out that in order to write www.silviuaavram.com in a browser and actually access the blog website, its production code needs to find its way back to the browser. Also, every time the blog receives an update and, as consequence, has the source code changed, the production code needs to be updated as well. Otherwise, we will read an outdated blog, which is totally unacceptable.

Consequently, a computer on the internet needs to be able to download the blog source code from GitHub, build it into production code, and serve the resulting production code to the browser that wants to retrieve it. This magical computer is called a server. When someone requests a link in the browser, this server needs to be found and asked to ... serve ... the website production files. These files will be received by the browser, which will piece them together and show us the blog website.

For these daunting tasks we will choose Netlify, since I had a great experience with their service when hosting the downshift docsite.

GoDaddy

Finally, this server computer needs to be linked to the address www.silviuaavram.com through a service called the Domain Name System. As such, we need to buy the address (domain) silviuaavram.com on the internet and tell Netlify that it belongs to us. In turn, Netlify can provide the actual website content every time someone requests the www.silviuaavram.com. The process of mapping the internet address to the IP address of the Netlify server is done by the internet using the DNS. We will use GoDaddy to buy the silviuaavram.com domain.

And that's all I need, finally let's get to work!

Source Code Setup

There are some standard prerequesites that we need. Most FrontEnd developers already are familiar with them:

  • GitHub account.
  • Installed NodeJS and Git on the laptop.
  • VSCode.

If you don't have these set up, the internet is your friend in order to have them ready.

GitHub

We will create a new GitHub repository called silviuaavram, make it Public, with a node .gitignore file and an MIT license. Afterwards, We will download the repo to our laptop as well, so we can make changes locally and then save them on GitHub once they are done.

We will open VSCode with a terminal, navigate to a place where we want to have the repo, and download it:

git clone git@github.com:silviuaavram/silviuaavram.git

Docusaurus

Second step is to setup docusaurus locally according to the docusaurus documentation. We want to have the initial code generated by docusaurus in the folder we just downloaded from GitHub, so we will run the setup command like this:

npx create-docusaurus@latest silviuaavram classic --typescript
cd silviuaavram
npm start

It will create a standard docusuaurus setup, in the folder silviuaavram, using TypeScript instead of JavaScript for the related source files. After the files get created, we will navigate inside the folder and run the npm start command in order to see how the website looks right now. It will get deployed locally at the address http://localhost:3000.

For now, we want to save the generated files to GitHub, so we will stage all of them, create a commit from the staged files, and push the changes to the remote GitHub repository.

git add -A
git commit -m "docusaurus setup"
git push

If you are unsure about the git-related concepts like commit, push, clone, stage etc. the Internet is, again, your friend.

Final Setup Tweaks

We will remove the package-lock.json file and create, instead, an .npmrc file with the content below, so we don't get the package-lock.json generated again. It is a matter of personal preference not to have one, mostly because there won't be many people working on this blog.

package-lock=false

We already have the .gitignore file generated, but we will also add a line with .docusaurus at the end of it, since this is going to be a directory with generated code, and we don't want to save it on GitHub.

Since I like to format my code automatically as I write it, we will be using the prettier utility, along with a configuration file from kcd-scripts. Only the prettier extension from VSCode is useful, so we will not install prettier for now. But we will install kcd-scripts as a developer dependency.

npm install --save-dev kcd-scripts

And we will also create a prettier.config.js file in the root with the following content:

// this is really only here for editor integrations
module.exports = require('kcd-scripts/prettier')

Now, every time we run the formatting command in VSCode, the code will get formatted according to the formatting rules from kcd-scripts. Great!

Removing the Docs. Defaulting the Blog.

Let's now shave the parts of our app that we don't need. Most of these steps are done according to Docusaurus' official documentation, as it's very well written.

Since we only need the blog part of the application, we are going to delete the support for documentation (docs). In docusaurus.config.js, we will look for the config.presets. Inside it, there is a docs entry, whose value is an object. We will replace the object with false. This should remove the docs part from the website.

Changing the value will result in a strange error displayed in our locally deployed application. To fix the error, we need to add routeBasePath: '/' to the blog object (next to the docs entry). We will also remove the first item in config.themeConfig.navbar.items (the one with type 'doc').

We also need to remove a few files as well. First and foremost, all the content in the src/pages directory should be deleted, since they are related to the docs part of the app. We can clear src/components as well, since these components are used only in the pages we just deleted. Oh, and we also won't need sidebars.js anymore, so we can get rid of that.

These changes should remove all the references to the docs, and the error should be fixed. We also made the blog page as default, so when we navigate to localhost:3000, it will display the blog. Fantastic!

Back in our docusaurus.config.js file, we need to also change the navbar item pointing to the blog to point to '/' instead of '/blog', due to our previous change with routeBasePath. We should also deal with the footer section. This one is really easy, for now, as we will only remove the whole links part, keeping only the style and copyright. The links to social media stuff will be added a bit later in the navigation bar.

So far, the config file should look like this:

docusaurus.config.js
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion

const lightCodeTheme = require('prism-react-renderer/themes/github')
const darkCodeTheme = require('prism-react-renderer/themes/dracula')

/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'My Site',
tagline: 'Dinosaurs are cool',
url: 'https://your-docusaurus-test-site.com',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',

// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'facebook', // Usually your GitHub org/user name.
projectName: 'docusaurus', // Usually your repo name.

// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en'],
},

presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: false,
blog: {
routeBasePath: '/',
showReadingTime: true,
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],

themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
title: 'My Site',
logo: {
alt: 'My Site Logo',
src: 'img/logo.svg',
},
items: [
{to: '/', label: 'Blog', position: 'left'},
{
href: 'https://github.com/facebook/docusaurus',
label: 'GitHub',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Community',
items: [
{
label: 'Stack Overflow',
href: 'https://stackoverflow.com/questions/tagged/docusaurus',
},
{
label: 'Discord',
href: 'https://discordapp.com/invite/docusaurus',
},
{
label: 'Twitter',
href: 'https://twitter.com/docusaurus',
},
],
},
{
title: 'More',
items: [
{
label: 'Blog',
to: '/',
},
{
label: 'GitHub',
href: 'https://github.com/facebook/docusaurus',
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
}

module.exports = config

Making it Personal

We're in a good spot right now, as we only have the Blog functionality in our app. However, our blog is about Docusaurus, while we actually want to write personal stuff here. Let's apply some cosmetic surgery and make it beautiful.

Back in our beloved docusaurus.config.js file, we will add our own information to the relevant fields.

  • title should be Silviu Alexandru Avram.
  • tagline should be Silviu's personal blog, focused on technology and lifestyle.

    both the title and tagline end up in the HTML file in their appropriate <head> child elements.

  • url is our domain that we just bought from Mr. GoDaddy, https://silviuaavram.com.
  • favicon becomes img/favicon.png.

    The favicon is placed in the Browser tab before the blog title. To create this favicon as a .png file, I used Adobe Illustrator. Guess what's my favorite drink.

  • organizationName should be the GitHub user name, here silviuaavram.
  • projectName should be the repository name, here also silviuaavram.
  • editURl from the blog object should point to our GitHub repository, specifically 'https://github.com/silviuaavram/silviuaavram/tree/main'

    The edit URL is a very handy feature in case anyone wants to edit the content of a blog post directly from the website.

  • colorMode should have it's defaultMode dark, as we want, by default, the website to be shown in the dark color mode. We're so dark right now.

    Color mode can still be switched to ligh from the toggle button in the upper right color of the screen.

  • navBar should have the following changes:
    • title should be, obviously, Silviu Alexandru Avram, which is actually the text content of the upper left link to the home page.
    • logo should be displayed next to the title, and it should be the blog's logo picture.
        logo: {
      alt: 'silviuaavram logo with a tequila glass and blog owner name initials',
      src: 'img/logo.png',
      },

      I used Adobe Ilustrator here as well, in order to create a logo for the website. Both the favicon and the logo files are saved in the static/img folder.

    • the navbar items should contain, on the left side, next to the logo, the links to the Blog and the About pages.
        items: [
      {to: '/', label: 'Blog', position: 'left'},
      {to: '/about', label: 'About', position: 'left'},
      // next items.
    • on the right part of the navbar, before the dark/light mode switch button, I will place the links to all my social media contacts: GitHub, Linkedin, Instagram and Twitter. This part involves a little more work than just adding the information below to the config file. The process is detailed in the next section.
        {
      href: 'https://github.com/silviuaavram',
      label: 'GitHub',
      position: 'right',
      icon: 'github',
      },
      // next items.
    • last, but not least, we will change the copyright text in the footer to include the author's name.

With all these changes done, our config file should look more or less like this:

docusaurus.config.js
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion

const lightCodeTheme = require('prism-react-renderer/themes/github')
const darkCodeTheme = require('prism-react-renderer/themes/dracula')

/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Silviu Alexandru Avram',
tagline: `Silviu's personal blog, focused on technology and lifestyle.`,
url: 'https://silviuaavram.com',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.png',

// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'silviuaavram', // Usually your GitHub org/user name.
projectName: 'silviuaavram', // Usually your repo name.

// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en'],
},

presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: false,
blog: {
routeBasePath: '/',
showReadingTime: true,
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl: 'https://github.com/silviuaavram/silviuaavram/tree/main',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],

themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
colorMode: {
defaultMode: 'dark',
},
navbar: {
title: 'Silviu Alexandru Avram',
logo: {
alt: 'silviuaavram logo with a tequila glass and blog owner name initials',
src: 'img/logo.png',
},
items: [
{to: '/', label: 'Blog', position: 'left'},
{to: '/about', label: 'About', position: 'left'},
{
href: 'https://github.com/silviuaavram',
label: 'GitHub',
position: 'right',
icon: 'github',
},
{
href: 'https://www.linkedin.com/in/silviuaavram',
label: 'LinkedIn',
position: 'right',
icon: 'linkedin',
},
{
href: 'https://www.instagram.com/silviuaavram/',
label: 'Instagram',
position: 'right',
icon: 'instagram',
},
{
href: 'https://twitter.com/silviuaavram',
label: 'Twitter',
position: 'right',
icon: 'twitter',
},
],
},
footer: {
style: 'dark',
copyright: `Copyright © ${new Date().getFullYear()} Silviu Alexandru Avram. Built with Docusaurus.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
}

module.exports = config

To add a link to the author's Twitter account in our blog's navigation bar, we can add the following object to themeConfig.navbar.items in docusaurus.config.js.

  {
href: 'https://twitter.com/silviuaavram',
label: "Silviu's Twitter link",
position: 'right',
icon: 'twitter',
},

By default, if we add an item to the navbar items, it will get shown on our website as link with the Silviu's Twitter link text and an external link icon next to it. That's acceptable, but let's create something thats more sophisticated than that. What we want to show is the actual logo of the social media that we're linking to. I mentioned above that Docusaurus allows the customisation of different parts of the layout and content for the website using a technique called Swizzling, and that's what we're going to do. We are going to Swizzle the navigation component in order to change it according to our needs.

We will run the command below in the terminal for a list of React components that can be swizzled.

npm run swizzle -- --list

In the long list of customizable components, we find NavbarItem/NavbarNavLink that represents the icon link we want to change. That is what we're going to sizzle, my nizzle.

There are 2 ways to swizzle:

  1. wrap the corresponding react component into another component, which makes sense if we want to augment it with our own custom stuff.
  2. eject the component completely, so we can change its default implementation.

We will do the second part, as we want to remove the link text, the ugly icon, and return just our own cool social media icon.

npm run swizzle @docusaurus/theme-classic NavbarItem/NavbarNavLink -- --eject

And confirm that yes, we know what we are doing. We will hit the Enter key slightly harder than usual, to show that we mean business.

Magic! In our src/theme folder we got a generated NavbarItem folder with the NavbarNavLink.js file. The file contains the default implementation for the navigation bar link, which is the element that we want to change if the link is a social media link. Notice that, in our docusaurus.config.js, for each social media element, we are also passing an icon property, which represents information about what icon we want to show for that link.

After skimming the code here and understand what's what, we find the default implementation for the external link. As we expected, it outputs the text via the label (in this case, "Silviu's Twitter link"), and if it's an external link (here it is external) it will show the IconExternalLink component next to it, which outputs the icon with the arrow.

  children: (
<>
{label}
{isExternalLink && (
<IconExternalLink
{...(isDropdownLink && {width: 12, height: 12})}
/>
)}
</>
),

We will change this code and only return a social media icon if, from the configuration file, we identify that the link we want to show is a social media link. Otherwise, we will return the default implementation. In order to be sure that we are dealing with a social media link, we will check its icon prop, since, from the configuration file, we will only pass icon to the social media links. The code becomes:

  children: icon ? (
<SocialMediaIcon type={icon} aria-label={label} className={styles.iconImg} />
) : (
<>
{label}
{isExternalLink && (
<IconExternalLink
{...(isDropdownLink && {width: 12, height: 12})}
/>
)}
</>
),

And we will also create a SocialMediaIcon component like the one below, where each variation will return an SVG output representing the icon tiself:

type SocialMediaIconType = 'twitter' | 'instagram' | 'github' | 'linkedin'

export type SocialMediaIconProps = React.ComponentPropsWithoutRef<'svg'> & {
type: SocialMediaIconType
}

const SocialMediaIconComponent: Record<
SocialMediaIconType,
React.FunctionComponent<React.ComponentPropsWithoutRef<'svg'>>
> = {
github: GitHub,
linkedin: LinkedIn,
twitter: Twitter,
instagram: Instagram,
}

export function SocialMediaIcon({
type,
...rest
}: SocialMediaIconProps): JSX.Element {
const IconComponent = SocialMediaIconComponent[type]

return <IconComponent {...rest} />
}

And one of the icons can be:

export default function GitHub(
props: React.ComponentPropsWithoutRef<'svg'>,
): JSX.Element {
return (
<svg
height="24"
width="24"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title />
<g
data-name="github coding dev developer"
id="github_coding_dev_developer"
>
<path
fill="currentColor"
d="M16,3a13,13,0,0,0-3.46,25.53,1,1,0,1,0,.53-1.92,11,11,0,1,1,7-.4,15.85,15.85,0,0,0-.3-3.92A6.27,6.27,0,0,0,24.68,16a6.42,6.42,0,0,0-1.05-3.87,7.09,7.09,0,0,0-.4-3.36,1,1,0,0,0-1.1-.67,8,8,0,0,0-3.37,1.28A11.35,11.35,0,0,0,16,9a13.09,13.09,0,0,0-3,.43A5.74,5.74,0,0,0,9.62,8.25a1,1,0,0,0-1,.66,7.06,7.06,0,0,0-.37,3.19A7.15,7.15,0,0,0,7.2,16a6.66,6.66,0,0,0,5,6.28,7.43,7.43,0,0,0-.15.79c-1,.06-1.58-.55-2.32-1.48a3.45,3.45,0,0,0-1.94-1.53,1,1,0,0,0-1.15.76A1,1,0,0,0,7.35,22c.16,0,.55.52.77.81a4.74,4.74,0,0,0,3.75,2.25,4.83,4.83,0,0,0,1.3-.18h0a1,1,0,0,0,.29-.14l0,0a.72.72,0,0,0,.18-.21.34.34,0,0,0,.08-.09.85.85,0,0,0,.06-.17,1.52,1.52,0,0,0,.06-.2v0a4.11,4.11,0,0,1,.46-1.91,1,1,0,0,0-.76-1.65A4.6,4.6,0,0,1,9.2,16a4.84,4.84,0,0,1,.87-3,1,1,0,0,0,.24-.83,5,5,0,0,1,0-1.85,3.59,3.59,0,0,1,1.74.92,1,1,0,0,0,1,.23A12.49,12.49,0,0,1,16,11a9.91,9.91,0,0,1,2.65.43,1,1,0,0,0,1-.18,5,5,0,0,1,2-1,4.11,4.11,0,0,1,0,1.91,1.05,1.05,0,0,0,.32,1A4,4,0,0,1,22.68,16a4.29,4.29,0,0,1-4.41,4.46,1,1,0,0,0-.94.65,1,1,0,0,0,.28,1.11c.59.51.5,4,.47,5.36a1,1,0,0,0,.38.81,1,1,0,0,0,.62.21,1.07,1.07,0,0,0,.25,0A13,13,0,0,0,16,3Z"
/>
</g>
</svg>
)
}

The actual SVG code is copied from a website that provides these icons for free. I chose iconfinder.com as the place to get the icons. Many thanks to them!

About Section and Images

Let's begin the creative journey by writing the About page. It's pretty standard across blogs such as ours, since it reveals a bit about the author, their experience, what they enjoy doing etc.

We will create an about.tsx file in the pages directory, with the following content, for now:

import React from 'react'

export default function About() {
return <div>Hello from Silviu!</div>
}

With Docusaurus' magic, when we navigate to localhost:3001/about, or by clicking the About link in the navbar that we just created, we will be greeted by a brand new page created for us automatically, displaying the Hello from Silviu message.

We will structure the About page in multiple sections. Each section will contain one image and one subsection with text paragraphs. Since the page is going to be built fully by us, it means that we are responsible for making it look great on any device. It must look great on a wide computer screen as well as on a small phone display. To achieve this, we will use responsive design techniques, with relative sizes and layout shifts. The styling will be done with a separate CSS file, about.module.css, which will contain the CSS declarations. These declarations will be imported in the .tsx file and applied to the elements that need to be styled, like below:

  // in about.tsx
<section className={`${styles.sectionContent} ${styles.sectionLargerContent}`}>
/* in about.module.css */
.sectionContent {
flex-grow: 1;
}

.sectionLargerContent {
flex-basis: 540px;
}

The layout we want to achieve is the following:

  • on small screens, add both the image and content on the same column.
  • on wide enough screens, move the content and image side by side on separate columns.
  • on all screens, make the image and content sizes dynamic, so they can grow along with the display size.
  • in the cases where we consider that the sections width is large enough, we will prevent the sections from growing, and instead add a dynamic horizontal margin between them and the screen edge, so they stay centered.

After carefully observing how our content looks on different screen widths, we will create the following break points in our app:

  1. up until 640 pixels, we will have only one column, for both images and content, and the column will grow as big as the screen.
  2. between 640 pixels and 1020 pixels, we will keep the same 1-column design, but we will stop it from growing as well. Instead, we will center this column, and add left and right margins to compensate for the rest of the width.
  3. 1020 pixels and 1280 pixels, we will perform a layout shift, and we will move the images and the content sections in separate columns, side by side. We will also let them grow dynamically if there is more space to go around.
  4. at 1280 pixels and above, we will keep the sections with 2 columns, but we will block their width at 1280 pixels, and perform the same horizontal margin like we did on point 2.

The important thing to know here is that we will choose to display responsive images. We will serve different sizes of images for different size of displays, as it is not necesary to send, over the network, a full size image if you are viewing the page on a phone.

For example, this will be the markup for the first author image:

<img
className={`${styles.sectionImage} ${styles.sectionPortraitImage}`}
alt="silviu sitting in front of to the fountain in piazza de ferrari in genoa"
src="/img/pictures/silviu-about-the-author-1800w.jpg"
srcSet="/img/pictures/about-author-400w.jpg 400w, /img/pictures/about-author-600w.jpg 600w, /img/pictures/about-author-800w.jpg 800w, /img/pictures/about-author-1000w.jpg 1000w, /img/pictures/about-author-1200w.jpg 1200w, /img/pictures/about-author-1500w.jpg 1500w, /img/pictures/about-author-1800w.jpg 1800w"
sizes="(max-width: 639px) 100vw, (min-width: 640px) and (max-width: 1019px) 608px, (min-width: 1020px) and (max-width: 1279px) 40vw, (min-width: 1280px) 522px"
/>

Whoa, whoa, whoa! The only thing we can tell from the markup above is the arrogant alt text for the image. But not to worry, here's what's happening. After playing around with the page in the Chrome DevTools' Device Toolbar, we can check what size will our image take depending on the size of the viewport and the layout that's displayed. Consequently, when our image shares the same column with the text content (up until 1020px width), its width varies between 0 and 608 pixels. When it has a separate column from the text content, its width varies between 415 and 522 pixels.

With this data gathered successfully, we will fill the sizes attribute of the <img> element, informing it about its size. By itself, the <img> cannot know this information, since it's oblivious to the css that's going to be apllied to it. So, with the sizes atttribute, we are helping it with this information.

Now, we also need to provide our <img> friend with a multitude of image sizes to choose from, with the help of the srcSet attribute. Our friend is smart enough to do the math, at least, and it will calculate the viewport width, multiply it by the DPI factor of the device, check via the sizes attribute what picture size it needs, and chooses the appropriate one via srcSet. And by appropriate, I mean the smallest one possible without making any compromise in quality.

For example, on a 3x DPI ratio phone with a 390 pixels width screen, it should pick the 1200w picture to display, since the picture itself uses the whole screen horizontally.

We will create, consequently, different picture copies, one for each size. I used Image Resizer since I was pretty happy with the result and the experience for generating the images we need. We will add the downloaded pictures in the static/img/pictures folder.

With the responsive images part dealt with, we can focus on writing and finish the author page. For example, this is the markdown for the header part of the page, with both paragraphs and a picture next to them.

Header Code.
<header>
<article>
<h2>About the author</h2>
<section className={styles.aboutTheAuthor}>
<section>
<p>
Hi, I'm Silviu, and I'm a software engineer 👨‍💻. When I'm not coding, I
spend my time playing basketball 🏀, salsa dancing 🕺 and drinking
cofee ☕️. I also drink coffee when coding, so there's that.
</p>
<p>
I believe that writing is an important way to improve myself and
others. If I write about a particular subject, without someone
explicitly forcing me to do it, it means that I aim to understand that
subject well enough. And I consider it important enough to share it
with someone else. It's the reason why I built this blog, to thing and
write about stuff that is helpful to me and can also be helpful to
others.
</p>
<figure>
<blockquote cite="https://youtu.be/bfDOoADCfkg">
<p>
<i>
Thinking makes you act effectively in the world. Thinking makes
you win the battles you undertake, and those could be battles
for good things. If you can think and speak and write, you are
absolutely deadly. Nothing can get in your way.
</i>
</p>
</blockquote>
<figcaption>
—Jordan Peterson, <cite>The Power of Writing</cite>
</figcaption>
</figure>
</section>
<img
className={styles.authorImage}
alt="silviu sitting in front of to the fountain in piazza de ferrari in genoa"
src="/img/pictures/silviu-about-the-author-1080w.jpg"
srcSet="/img/pictures/silviu-about-the-austyles.articleInContentthor-360w.jpg, /img/pictures/silviu-about-the-author-720w.jpg 2x, /img/pictures/silviu-about-the-author-1080w.jpg 3x"
width={360}
/>
</section>
</article>
</header>

Oh, and if you're interested in the resulting css styles file, here it is:

about.module.css
.mainContainer {
max-width: 640px;
margin: 2rem auto;
width: 100%;
padding: 0 1rem;
text-align: justify;
}

.sectionContainer {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}

.sectionImage {
flex-grow: 1;
object-fit: contain;
align-self: start;
min-width: 0;
}

.sectionPortraitImage,
.sectionSmallerContent {
flex-basis: 410px;
}

.sectionLandscapeImage,
.sectionLargerContent {
flex-basis: 550px;
}

.sectionContent {
flex-grow: 1;
}

@media (min-width: 1020px) {
.mainContainer {
max-width: 1280px;
}

.articleInContent {
margin-bottom: 2.5rem;
}

.educationAndInternshipsImage,
.softwareEngineeringMicrosoftImage {
order: 1;
}
}

@media (min-width: 1020px) and (max-width: 1279px) {
.hobbiesContent {
flex-basis: 980px;
}

.hobbiesMargaritaImage,
.hobbiesCapuccinoImage {
order: 1;
}
}

@media (min-width: 1280px) {
.hobbiesContent,
.hobbiesMargaritaImage,
.hobbiesCapuccinoImage {
flex-basis: 400px;
}
}

Writing the Post

In the blog folder, we already have some illustrative examples on how to write posts. We will review and delete them afterwards, since we only want to publish or own stuff. We have 2 options to write the blog post:

  1. Create an .md file directly in the blog folder.
  2. Create a folder containing at least an index.md file.

We will choose the second option since we also want to include the image along with the blog post file. We will name the folder using the format suggested in the illustrative examples, 2022-11-26-building-this-blog. The first part is the date of writing the post, the second is the blog post name, all in kebab case. We will also add the mountain-cabin.jpg image that will be included in the post.

Consequently, this blog post will contain the actual text content, an image that should be illustrative for the post, and some code snippets that are going to be relevant to following along the technical process.

Final Thoughts

And we're done! If you've made it until the end, you're a hero, and the Gryffindor House receives 10 points! We have covered a lot in this post:

  • the technology stack used to create and publish our digital online blog.
  • setting up the Docusaurus template with our own content in order to make the blog about us.
  • techniques for responsive images, accessible icons and components swizzling.
  • writing the About page.
  • writing the actual blog post.

The reason I wrote this blog post is not so much as to give a technical tutorial, although I did try to be as explicit as possible with the process itself. My sincere goal is to encourage everyone to build their own blog and start writing about things you are passionate about. I do want to read all about it!

Good luck!