<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Kieran&apos;s Web Dev Blog</title><description>Learn by building! I write about frontend UI/UX, React/Next.js, software testing, web accessibility, and more.</description><link>https://kieranroberts.dev/</link><language>en-us</language><item><title>How To Persist Style Changes Through Reloads Using Overrides In Dev Tools</title><link>https://kieranroberts.dev/blog/how-to-persist-style-changes-through-reloads-using-overrides-in-dev-tools/</link><guid isPermaLink="true">https://kieranroberts.dev/blog/how-to-persist-style-changes-through-reloads-using-overrides-in-dev-tools/</guid><description>This article will show you how to make CSS style changes that persist through reloads using the Chrome browser dev tools? Let&apos;s see how it&apos;s done!</description><pubDate>Sat, 11 Mar 2023 19:21:49 GMT</pubDate><content:encoded>Did you know it is possible to make persistent-through-reload CSS style changes using the in-browser dev tools? This method can also be used for other needs, so it&apos;s a really useful tool to have in your debugging arsenal.

Recently I came across a UI bug that was only visible in our staging environment, and not in my localhost environment. Persisting style changes this way was something I needed to help solve the bug efficiently and cleanly. It&apos;s not something you&apos;ll likely need very often but is an awesome tool to have in your debugging arsenal.

## TLDR

Make style changes persist through a reload using the in-browser dev tools.

- Open Chrome dev tools
- Visit **Sources tab** -&amp;gt; **Overrides sub-tab** -&amp;gt; **Select folder for overrides -&amp;gt;** Click **Allow -&amp;gt;** Select **Enable Local Overrides -&amp;gt;** Use **Sources/Page tab to select files -&amp;gt;** Right click **Save for Overrides -&amp;gt;** Make your style changes
- User the **Sources** tab for style changes if the source of the styles (CSS) is coming from an HTML file.
- Profit!

## What bug was I facing?

The bug itself was not so interesting, I&apos;ll be honest. But the conditions around the bug made it a more challenging fix to solve efficiently.

We often make use of staging environments as a midpoint between local development and production. This is currently the case for our Hashnode-powered blogs. While the team and I were testing some of these updates in staging, it was reported to me that there was a layout shift during a page load for our new navbar which I was responsible for.

Although the bug itself would likely be adding/removing a style or two, knowing exactly which style needed to be added or removed was problematic without visualisation of any changes. I couldn&apos;t see this in my localhost development.

Our staging environment is pretty fast these days, much closer to production than the development environment which helps us catch issues like this before we ship them. I set out to investigate and since I couldn&apos;t replicate using localhost, I would have to debug the issue using the staging environment.

The problem was that I didn&apos;t want to push changes to the staging branch that I thought **_might_** fix the problem, I wanted to know that it was **_sure_** to fix the problem. Better to avoid pushing commits and possibly having to revert them, or end up with a cluttered history of commits with descriptions like this:

&gt; fix: pls fix layout shift for main nav 🙃

## Solution: Using the Chrome browser dev tools

Like many of you, I often use the in-browser dev tools as an efficient method of trying out different style changes at speed. It&apos;s a great way of making a multitude of quick changes without touching the codebase. If you don&apos;t use it often, give it a go.

Any changes you make to styles this way will disappear when you reload the page. That&apos;s a problem when you need those style changes to persist after the reload. I needed to see that the change I would make was fixing the layout shift. That&apos;s where the Chrome browser dev tools came to my rescue.

### Chrome Dev Tools &apos;Sources&apos; Tab

We can preserve style changes through reload using the **Sources** tab of the Chrome Dev tools. This feature was released a few years back but having just come across this myself, some of you might also be seeing it for the first time.

I&apos;ll be using the Chrome dev tools for this tutorial.

- Open up the dev tools through a right click -&amp;gt; **Inspect**
- Navigate first to **Sources,** then the **Overrides** sub-tab.
  Here we can select a directory on our filesystem that will save style changes we make on whatever domain you need.
- Then click on **Select folder for overrides**. Feel free to create a new directory for your changes. You can see all this below.

![Chrome Dev Tools, pointing to the Sources Tab and overrides option](https://cdn.kieranroberts.dev/blog/how-to-persist-style-changes-through-reloads-using-overrides-in-dev-tools-1.webp)

- After selecting a file you&apos;ll see a warning. Click on **Allow** and don&apos;t expose sensitive information as it warns.

![Chrome Dev Tools, sensitive information warning](https://cdn.kieranroberts.dev/blog/how-to-persist-style-changes-through-reloads-using-overrides-in-dev-tools-2.webp)

- After clicking you&apos;ll see the directory under the **Overrides** sub-tab. Ensure the checkbox named **Enable Local Overrides** is selected.

![Chrome Dev Tools, enabling local overrides](https://cdn.kieranroberts.dev/blog/how-to-persist-style-changes-through-reloads-using-overrides-in-dev-tools-3.webp)

- Next up you need to select the sources you&apos;d like to save for overrides. Navigate to the **Page** sub-tab. Right-click on the source where you&apos;d like to make changes and click **Override content.**

![Chrome Dev Tools, saving file for overrides](https://cdn.kieranroberts.dev/blog/how-to-persist-style-changes-through-reloads-using-overrides-in-dev-tools-4.webp)

Now you&apos;re finally ready to make some styling changes.

The way you make these changes has some conditions If you make style changes in the Elements tab using the DOM (Document Object Model) tree, changes will not be saved and you should use the Sources tab instead. Find your file there and make edits.

Secondly, when editing CSS in the Styles pane, if the source of the CSS is an HTML file then any changes made won&apos;t be saved by DevTools. It&apos;s better to edit the HTML file in the Sources panel to ensure any changes are saved.

When an override is in effect you&apos;ll notice a little purple circle on top of the file icon and filename.

### What else can you use this for?

We can use local overrides for things other than style changes as well. Some of the use cases that come to mind are:

- Testing potential performance improvements
  - Using different fonts
  - Changing script load orders
- Trying out changes or debugging potential issues in external libraries

It&apos;s likely not something you&apos;ll need to use very often but will come in very handy in some of these specific use cases.

**Note:** It&apos;s a good idea to remove your overrides after you are done.

## Conclusion

Using this method I was able to run through some minor style changes very quickly and find/fix the culprit. It ended up being a highly time-efficient solution to a potentially frustrating problem.

It allowed me to cut the effort in debugging -&amp;gt; shipping by a considerable amount. I hope you find it useful in the future too.

See you next time 👋</content:encoded></item><item><title>Tutorial: Implement a Scroll-Translated, Dynamic Sticky Navbar in React</title><link>https://kieranroberts.dev/blog/tutorial-implement-a-scroll-translated-dynamic-sticky-navbar-in-react/</link><guid isPermaLink="true">https://kieranroberts.dev/blog/tutorial-implement-a-scroll-translated-dynamic-sticky-navbar-in-react/</guid><description>Learn to implement a dynamic, sticky, scroll-aware navbar in React with smooth transitions and accessibility optimization for reduced-motion preferences</description><pubDate>Thu, 02 Jan 2025 12:15:49 GMT</pubDate><content:encoded>As a frontend-focused developer, you&apos;ll spend a significant portion of your career working on navigation bars. These essential components are integral to almost every modern website and come in various implementations.

In this tutorial we will build a sticky navbar that hides as you scroll down using translation and reappears when you scroll up, based on scroll position calculations. It can provide quite a pleasant navigation experience as it maximizes screen space while keeping important links readily accessible. We will ensure it is accessible, as performant as possible, and leverage React hooks.

Let’s go.

## TLDR

Here’s how the final version functions: [Demo](https://www.loom.com/share/8cc092c9eff1468dafee223d61c698a2)

Here’s the final code:

```typescript
// hooks/useStickyHeader.ts

interface UseStickyHeaderProps {
	elRef: React.RefObject&lt;HTMLElement&gt;;
}

const TRANSLATE_BUFFER = 30; // in pixels
const QUERY_NAME = &apos;(prefers-reduced-motion: no-preference)&apos;;

export const useStickyHeader = ({ elRef }: UseStickyScrollProps) =&gt; {
	const [prefersReducedMotion, setPrefersReducedMotion] = useState(true);

	const scrollRef = useRef&lt;{ prevScrollTop: number; animation?: number }&gt;({
		prevScrollTop: 0,
		animation: undefined,
	});

	const getScrollDistance = ({ scrollY }: { scrollY: number }) =&gt; {
		const { prevScrollTop } = scrollRef.current;
		return scrollY - prevScrollTop;
	};

	const getHeaderTopValue = () =&gt; {
		const headerPosition = elRef.current?.getBoundingClientRect();
		return headerPosition?.top ?? 0;
	};

	const calculateTranslateValue = ({
		headerTop,
		scrollDistance,
	}: {
		headerTop: number;
		scrollDistance: number;
	}) =&gt; {
		const navHeight = (elRef.current?.offsetHeight || 0) + TRANSLATE_BUFFER;
		return Math.max(
			Math.min(
				headerTop + (scrollDistance &lt; 0 ? Math.abs(scrollDistance) : -Math.abs(scrollDistance)),
				0,
			),
			-navHeight,
		);
	};

	const onTranslate = () =&gt; {
		scrollRef.current.animation = requestAnimationFrame(() =&gt; {
			const curScrollTop = window.scrollY;
			const scrollDistance = getScrollDistance({ scrollY: curScrollTop });
			const headerTop = getHeaderTopValue();
			const translateAmount = calculateTranslateValue({
				headerTop,
				scrollDistance,
			});

			if (elRef.current) {
				elRef.current.style.transform = `translateY(${amount}px)`;
			}

			scrollRef.current.prevScrollTop = curScrollTop;
		});
	};

	useEffect(() =&gt; {
		if (prefersReducedMotion) {
			return;
		}
		window.addEventListener(&apos;scroll&apos;, onTranslate);

		return () =&gt; {
			window.removeEventListener(&apos;scroll&apos;, onTranslate);

			if (scrollRef.current.animation) cancelAnimationFrame(scrollRef.current.animation);
		};
	}, [prefersReducedMotion]);

	useEffect(() =&gt; {
		const mediaQueryList = window.matchMedia(QUERY_NAME);

		setPrefersReducedMotion(!mediaQueryList.matches);

		const updateMotionSettings = (event: MediaQueryListEvent) =&gt; {
			setPrefersReducedMotion(!event.matches);
		};

		mediaQueryList.addEventListener(&apos;change&apos;, updateMotionSettings);

		return () =&gt; {
			mediaQueryList.removeEventListener(&apos;change&apos;, updateMotionSettings);
		};
	}, []);
};
```

```typescript
// Navbar.tsx

import { useRef } from &apos;react&apos;;
import { useStickyHeader } from &apos;./hooks/useStickyHeader.ts&apos;;

export const Navbar = () =&gt; {
  const headerRef = React.useRef&lt;HTMLElement&gt;(null);
  useStickyHeader({ elRef: headerRef });

  return (
    &lt;header
      ref={headerRef}
      className=&quot;z-[9999] sticky top-0&quot;
    &gt;
      ...
    &lt;/header&gt;
  )
};
```

Here are key features:

1. When scrolling down it should gradually disappear above the fold as you would expect with any `static` positioned element that disappears with viewport scroll.
2. But as soon as you scroll up the slightest bit, it should start to reveal itself again until it’s fully visible in it’s normal `sticky` state.
3. The movement of the navbar will be handled by translating it up and down, we will need to do some calculations based on scroll positions for this.

## Setting Up Basic Navbar Requirements

The first step is to build your desired navbar. If you already have a working navbar on your site, that will work just fine. There are only a couple of things we must ensure for the UX to match what we have in the demo.

For the purpose of the tutorial, I am using Tailwind for styling and also TypeScript, but neither are required.

---

Our navbar will use `sticky` positioning. [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/position) defines sticky positioning as the following:

&gt; The element is positioned according to the normal flow of the document, and then offset relative to its _nearest scrolling ancestor_ and [containing block (nearest block-](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block)level ancestor), including table-related elements, based on the values of `top`, `right`, `bottom`, and `left`. The offset does not affect the position of any other elements.

We will vertically position the navbar at the top of the viewport using `top: 0px`.

Finally we’ll need to track the navbar element for later use when the core logic of the sticky nav is written. We’ll do that by adding a `ref` to the top level element of the navbar component. A high `z-index` value has also been added so the navbar is stacked on top of all other content.

```typescript
export const Navbar = () =&gt; {
  const headerRef = React.useRef&lt;HTMLElement&gt;(null);

  return (
    &lt;header
      ref={headerRef}
      className=&quot;z-[9999] sticky top-0&quot;
    &gt;
      ...
    &lt;/header&gt;
  )
};
```

Those are essential parts your navbar element should replicate. Next up we need to write the core logic for our desired UX. This will be done by writing a custom React hook. The core logic can be adapted to your requirements.

## `useStickyHeader` React Hook

Staying in the `Navbar` component for a second, will use our hook as follows:

```typescript
import { useRef } from &apos;react&apos;;
import { useStickyHeader } from &apos;./hooks/useStickyHeader.ts&apos;;

export const Navbar = () =&gt; {
  const headerRef = React.useRef&lt;HTMLElement&gt;(null);

  useStickyHeader({ elRef: headerRef });

  return (
    &lt;header
      ref={headerRef}
      className=&quot;z-[9999] sticky top-0&quot;
    &gt;
      ...
    &lt;/header&gt;
  )
};
```

Now we need to create the hook. Create a file called `useStickyHeader.ts` and define the basic outline of the hook:

```typescript
interface UseStickyScrollProps {
	elRef: React.RefObject&lt;HTMLElement&gt;;
}

export const useStickyHeader = ({ elRef }: UseStickyScrollProps) =&gt; {};
```

### How Will The Animation Work?

When scrolling down, we are going to translate the navbar upwards to a maximum translation distance of when it’s fully hidden (+ small offset). The navbar is hiding just above the top of the visible viewport so that when you start scrolling up, it immediately starts coming back into view. In that case we translate the navbar down again.

To help illustrate this, in the image below, consider the thick black line to be the top of the visible viewport. Meaning you can’t see anything above that line. This is where the navbar will be positioned when we have scrolled down and hidden the navbar from view. It’s just above the fold and ready to come right back into view.

![Scroll translated dynamic sticky navbar illustration](https://cdn.kieranroberts.dev/blog/tutorial-implement-a-scroll-translated-dynamic-sticky-navbar-in-react-1.webp)

### Implementing Scroll Based Logic

To do this precisely requires some calculations taking into account the distance that has been scrolled since the last check. Therefore we’ll make use of a `scroll` event listener. Let’s add this to our hook.

```typescript
interface UseStickyHeaderProps {
	elRef: React.RefObject&lt;HTMLElement&gt;;
}

export const useStickyHeader = ({ elRef }: UseStickyScrollProps) =&gt; {
	const onTranslate = () =&gt; {};

	useEffect(() =&gt; {
		window.addEventListener(&apos;scroll&apos;, onTranslate);

		return () =&gt; {
			window.removeEventListener(&apos;scroll&apos;, onTranslate);
		};
	}, []);
};
```

The `scroll` event listener was added inside a `useEffect` that will run on mount. Make sure to cleanup the event listener when unmounting, that’s what the `return` statement of a `useEffect` is for. An empty `onTranslate` function has also been created for use as the callback for the listener. Logic will be added there shortly.

Next up we need to write out translating code. For that we need to do some calculations. We need to know the number of pixels the document is currently scrolled vertically. This is given by `window.scrollY`.

Next we calculate how far we have scrolled since the last time the `onTranslate` function ran. It’s important to note that the `scroll` listener is not going to fire the `onTranslate` callback for every `1px` scrolled. The distance scrolled can be variable.

By tracking the `window.scrollY` value, we have access to the previous state when calculating the new translation value. A positive value means the user is scrolling down, a negative value means the user is scrolling up.

Let’s put this logic into action. Inside the hook we will write another function `getScrollDistance`:

```typescript
// Define a ref in the hook to keep track of previous scroll top value
const scrollRef = useRef&lt;{ prevScrollTop: number }&gt;({
	prevScrollTop: 0,
});

// Calculate the distance change from previous check
const getScrollDistance = ({ scrollY }: { scrollY: number }) =&gt; {
	const { prevScrollTop } = scrollRef.current;
	return scrollY - prevScrollTop;
};
```

and then call this function as part of `onTranslate`:

```typescript
const onTranslate = () =&gt; {
	const curScrollTop = window.scrollY;
	const scrollDistance = getScrollDistance({ scrollY: curScrollTop }); // We will use this in a moment

	scrollRef.current.prevScrollTop = curScrollTop; // Track our scroll top value
};
```

Another required value is the current vertical position of the navbar relative to the viewport. Create a function called `getHeaderTopValue` for this:

```typescript
const getHeaderTopValue = () =&gt; {
	const headerPosition = elRef.current?.getBoundingClientRect();
	return headerPosition?.top ?? 0; // ?? 0 is just a fallback in case the ref was not found
};
```

### Calculation Pseudocode

With that, all the information is ready to calculate the exact translation value. Before the calculation is written in code, let’s see it in pseudocode to understand what is happening:

```plaintext
Given:
- headerTop: current position of header from top of viewport
- scrollDistance: how far user has scrolled (positive = down, negative = up)
- navHeight: height of navigation + buffer

Step 1: Calculate scroll adjustment

IF scrollDistance is negative (scrolling up)
    adjustment = +|scrollDistance|    // Move header downward
ELSE (scrolling down)
    adjustment = -|scrollDistance|    // Move header upward

Step 2: Calculate new position

newPosition = headerTop + adjustment

Step 3: Clamp the value

maxValue = 0                         // Header can&apos;t go above viewport
minValue = -navHeight               // Header can&apos;t hide more than its height
finalPosition = CLAMP(newPosition, minValue, maxValue)
```

### Calculate Translate Value

Implementing the pseudocode, we end our with our translation value:

```typescript
const TRANSLATE_BUFFER = 30; // in pixels

const calculateTranslateValue = ({
	headerTop,
	scrollDistance,
}: {
	headerTop: number;
	scrollDistance: number;
}) =&gt; {
	const navHeight = (elRef.current?.offsetHeight || 0) + TRANSLATE_BUFFER;
	return Math.max(
		Math.min(
			headerTop + (scrollDistance &lt; 0 ? Math.abs(scrollDistance) : -Math.abs(scrollDistance)),
			0,
		),
		-navHeight,
	);
};
```

The `TRANSLATE_BUFFER` is not essential but I think it helps with the smoothness of the UX. It adds a small offset so the navbar is an extra 30px above the visible viewport. You can experiment with that value.

Implementing the translation calculation in `onTranslate`:

```typescript
const onTranslate = () =&gt; {
	const curScrollTop = window.scrollY;
	const scrollDistance = getScrollDistance({ scrollY: curScrollTop });
	const headerTop = getHeaderTopValue();
	const translateAmount = calculateTranslateValue({
		headerTop,
		scrollDistance,
	});

	scrollRef.current.prevScrollTop = curScrollTop;
};
```

The final step important is to actually perform the css translation with our calculated value. All we have to do is to use the navbar `ref` we defined earlier and update its `style.transform` property:

```typescript
const onTranslate = () =&gt; {
	requestAnimationFrame(() =&gt; {
		const curScrollTop = window.scrollY;
		const scrollDistance = getScrollDistance({ scrollY: curScrollTop });
		const headerTop = getHeaderTopValue();
		const translateAmount = calculateTranslateValue({
			headerTop,
			scrollDistance,
		});

		if (elRef.current) {
			// Move the navbar up or down in pixels
			elRef.current.style.transform = `translateY(${amount}px)`;
		}

		scrollRef.current.prevScrollTop = curScrollTop;
	});
};
```

Notice we wrap the entire function with the `window.requestAnimationFrame` method which tells the browser you wish to perform an animation. It requests the browser to call a user-supplied callback function before the next repaint.

This performance optimization helps prevents multiple unnecessary calculations within the same frame. Scroll events can fire at a very high rate so we need to optimise the performance. For example, if a user scrolls quickly triggering 100 or more scroll events in a timeframe of say 20ms, `requestAnimationFrame` will consolidate these into a single frame update. We can ensure our animations remain smooth at the optimal frame rate the device used by the user.

Let’s also expand the scroll `ref` to eventually allow animation cancelling:

```typescript
const scrollRef = useRef&lt;{ prevScrollTop: number; animation?: number }&gt;({
	prevScrollTop: 0,
	animation: undefined,
});
```

and then set a reference for our `requestAnimationFrame` callback using `scrollRef.current.animation`:

```typescript
const onTranslate = () =&gt; {
	scrollRef.current.animation = requestAnimationFrame(() =&gt; {
		const curScrollTop = window.scrollY;
		const scrollDistance = getScrollDistance({ scrollY: curScrollTop });
		const headerTop = getHeaderTopValue();
		const translateAmount = calculateTranslateValue({
			headerTop,
			scrollDistance,
		});

		if (elRef.current) {
			// Move the navbar up or down in pixels
			elRef.current.style.transform = `translateY(${amount}px)`;
		}

		scrollRef.current.prevScrollTop = curScrollTop;
	});
};
```

This now allows clean animation cancelling in `useEffect` cleanup:

```typescript
useEffect(() =&gt; {
	window.addEventListener(&apos;scroll&apos;, onTranslate);

	return () =&gt; {
		window.removeEventListener(&apos;scroll&apos;, onTranslate);
		if (scrollRef.current.animation) {
			cancelAnimationFrame(scrollRef.current.animation);
		}
	};
}, []);
```

Cancelling the animation frame is necessary because if the component unmounts while an animation frame is pending, you might end up trying to run `onTranslate` on an unmounted component which will probably lead to errors on memory leaks.

### Considering `prefers-reduced-motion` CSS Media Feature

An improvement we can make to the hook is to consider users to prefer to reduce animations when visiting sites. `prefers-reduced-motion` is a CSS media feature in this vein and here is how it described as per [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion):

&gt; The `prefers-reduced-motion` [CSS me](https://developer.mozilla.org/en-US/docs/Web/CSS)[dia feature is used to d](https://developer.mozilla.org/en-US/docs/Web/CSS/@media#media_features)etect if a user has enabled a setting on their device to minimize the amount of non-essential motion. The setting is used to convey to the browser on the device that the user prefers an interface that removes, reduces, or replaces motion-based animations.

The transitioning being performed on the navbar may be unwanted by users who prefer to be without animations. We can make use of this media query in the hook and prevent the translate from happening if they have this set on their device. Instead the header will just remain in a regular `sticky` state.

Here’s the additional logic for checking the user’s preference:

```typescript
const QUERY_NAME = &apos;(prefers-reduced-motion: no-preference)&apos;;

// Default to true
const [prefersReducedMotion, setPrefersReducedMotion] = useState(true);

useEffect(() =&gt; {
	const mediaQueryList = window.matchMedia(QUERY_NAME);

	setPrefersReducedMotion(!mediaQueryList.matches);

	const updateMotionSettings = (event: MediaQueryListEvent) =&gt; {
		setPrefersReducedMotion(!event.matches);
	};

	mediaQueryList.addEventListener(&apos;change&apos;, updateMotionSettings);

	return () =&gt; {
		mediaQueryList.removeEventListener(&apos;change&apos;, updateMotionSettings);
	};
}, []);
```

Here the user’s system setting is checked with `window.matchMedia(&quot;(prefers-reduced-motion: no-preference)&quot;)`. Specifically checking if they don’t have a preference with `prefers-reduced-motion: no-preference` and if it doesn’t match, we know they prefer to have reduced motion, hence we keep `setPrefersReducedMotion` to `true`.

Now all we have to do is prevent the scroll listeners from doing their magic in the case where the user wants to prevent animations.

```typescript
useEffect(() =&gt; {
	if (prefersReducedMotion) {
		return;
	}
	window.addEventListener(&apos;scroll&apos;, onTranslate);

	return () =&gt; {
		window.removeEventListener(&apos;scroll&apos;, onTranslate);

		if (scrollRef.current.animation) {
			cancelAnimationFrame(scrollRef.current.animation);
		}
	};
}, [prefersReducedMotion]);
```

Note we also need to add `prefersReducedMotion` to the dependency array so that changes to the value re-trigger the effect.

And that’s all! Now you should have an accessible, optimized, scroll-positioned navbar with a smooth transition.

You could also extract the media query logic into a separate hook for use elsewhere, or open up our hook for use with other components. Feel free to experiment!

## Final Implementation

Here is the full implementation:

```typescript
// hooks/useStickyHeader.ts

interface UseStickyHeaderProps {
	elRef: React.RefObject&lt;HTMLElement&gt;;
}

const TRANSLATE_BUFFER = 30; // in pixels
const QUERY_NAME = &apos;(prefers-reduced-motion: no-preference)&apos;;

export const useStickyHeader = ({ elRef }: UseStickyScrollProps) =&gt; {
	const [prefersReducedMotion, setPrefersReducedMotion] = useState(true);

	const scrollRef = useRef&lt;{ prevScrollTop: number; animation?: number }&gt;({
		prevScrollTop: 0,
		animation: undefined,
	});

	const getScrollDistance = ({ scrollY }: { scrollY: number }) =&gt; {
		const { prevScrollTop } = scrollRef.current;
		return scrollY - prevScrollTop;
	};

	const getHeaderTopValue = () =&gt; {
		const headerPosition = elRef.current?.getBoundingClientRect();
		return headerPosition?.top ?? 0;
	};

	const calculateTranslateValue = ({
		headerTop,
		scrollDistance,
	}: {
		headerTop: number;
		scrollDistance: number;
	}) =&gt; {
		const navHeight = (elRef.current?.offsetHeight || 0) + TRANSLATE_BUFFER;
		return Math.max(
			Math.min(
				headerTop + (scrollDistance &lt; 0 ? Math.abs(scrollDistance) : -Math.abs(scrollDistance)),
				0,
			),
			-navHeight,
		);
	};

	const onTranslate = () =&gt; {
		scrollRef.current.animation = requestAnimationFrame(() =&gt; {
			const curScrollTop = window.scrollY;
			const scrollDistance = getScrollDistance({ scrollY: curScrollTop });
			const headerTop = getHeaderTopValue();
			const translateAmount = calculateTranslateValue({
				headerTop,
				scrollDistance,
			});

			if (elRef.current) {
				elRef.current.style.transform = `translateY(${amount}px)`;
			}

			scrollRef.current.prevScrollTop = curScrollTop;
		});
	};

	useEffect(() =&gt; {
		if (prefersReducedMotion) {
			return;
		}
		window.addEventListener(&apos;scroll&apos;, onTranslate);

		return () =&gt; {
			window.removeEventListener(&apos;scroll&apos;, onTranslate);

			if (scrollRef.current.animation) cancelAnimationFrame(scrollRef.current.animation);
		};
	}, [prefersReducedMotion]);

	useEffect(() =&gt; {
		const mediaQueryList = window.matchMedia(QUERY_NAME);

		setPrefersReducedMotion(!mediaQueryList.matches);

		const updateMotionSettings = (event: MediaQueryListEvent) =&gt; {
			setPrefersReducedMotion(!event.matches);
		};

		mediaQueryList.addEventListener(&apos;change&apos;, updateMotionSettings);

		return () =&gt; {
			mediaQueryList.removeEventListener(&apos;change&apos;, updateMotionSettings);
		};
	}, []);
};
```

```typescript
// Navbar.tsx

import { useRef } from &apos;react&apos;;
import { useStickyHeader } from &apos;./hooks/useStickyHeader.ts&apos;;

export const Navbar = () =&gt; {
  const headerRef = React.useRef&lt;HTMLElement&gt;(null);
  useStickyHeader({ elRef: headerRef });

  return (
    &lt;header
      ref={headerRef}
      className=&quot;z-[9999] sticky top-0&quot;
    &gt;
      ...
    &lt;/header&gt;
  )
};
```

## Summary

In conclusion, implementing a scroll-translated, dynamic sticky navbar in React can bring a nice touch to your website. By leveraging React hooks and considering user preferences for reduced motion, developers can create a smooth, responsive, and accessible navigation experience.

Feel free to share the article if it was helpful.</content:encoded></item><item><title>kieranroberts.dev: My New Dev Portfolio Built With Astro, TailwindCSS, and TypeScript</title><link>https://kieranroberts.dev/blog/kieranrobertsdev-my-new-dev-portfolio-built-with-astro-tailwindcss-and-typescript/</link><guid isPermaLink="true">https://kieranroberts.dev/blog/kieranrobertsdev-my-new-dev-portfolio-built-with-astro-tailwindcss-and-typescript/</guid><description>A case study on building my developer portfolio &apos;kieranroberts.dev&apos; using Astro, TailwindCSS, and TypeScript.</description><pubDate>Thu, 16 Jan 2025 12:50:52 GMT</pubDate><content:encoded>I decided to build a new personal developer portfolio for myself. Somewhere to showcase my skills, experience, projects etc. It is now deployed at [kieranroberts.dev](https://kieranroberts.dev/) and I wanted to write up a small case study reflecting on the project. Talk about what went well, what I might do differently next time, and what can be improved. I will also discuss the tech involved and the thinking behind certain decisions.

I’ve had my eye on [Astro](https://astro.build/) for a while, knowing it would be a perfect fit for a simple content-focused site like this. Leveraging TailwindCSS, TypeScript, and Hashnode API’s for the remaining work, here is a look behind the curtain of my new portfolio and how it was put together.

## The ‘final’ product

I’m hesitant to say ‘final’ since it will be iterated on further, but we can say the initial MVP is done:

[kieranroberts.dev](https://kieranroberts.dev/)

![kieranroberts.dev home page hero section](https://cdn.kieranroberts.dev/blog/kieranrobertsdev-my-new-dev-portfolio-built-with-astro-tailwindcss-and-typescript-1.webp)

## The problem

This a list of notes/requirements I kept in mind before beginning the project:

1. I will be a simple content focused site and minimal dynamic elements.
2. Start out as a simple one-pager.
3. Fast.
4. Accessible.
5. Be search engine optimised.
6. Clean/simple design style to not get bogged down in design hell.
7. Light/dark theme.
8. Free hosting.

## Site content

The page would have the following sections at a minimum:

1. Landing page hero.
2. Career &amp; experience (relevant stuff only with CV download).
3. Blog (recent articles only and a link to the blog).
4. Testimonials
5. Personal projects.
6. Additional achievements.

With that mind, it was time to pick the right tools for the job.

## Tech stack

&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;&lt;summary&gt;Astro&lt;/summary&gt;&lt;div data-type=&quot;detailsContent&quot;&gt;In early 2024 I attended &lt;a target=&quot;_self&quot; rel=&quot;noopener noreferrer nofollow&quot; href=&quot;https://kongresjs.pl/&quot; style=&quot;pointer-events: none&quot;&gt;Kongres.js&lt;/a&gt; in Warsaw, Poland. There were talks on various tools and frameworks. One of which was Astro. Two of its major pluses is that it is optimised for performance SEO, among other things. Astro uses a server-first approach sending lightweight HTML to the browser, removing unnecessary JavaScript. This made it a perfect choice for this project. And in the future, if I decide to move my blog to &lt;code&gt;kieranroberts.dev/blog&lt;/code&gt;, Astro would be a great fit for this use case as well so future-proofing is covered.&lt;/div&gt;&lt;/details&gt;&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;&lt;summary&gt;TailwindCSS&lt;/summary&gt;&lt;div data-type=&quot;detailsContent&quot;&gt;For styling I like TailwindCSS. I love the speed at which I can build styled elements and I am already very comfortable with Tailwind so it would help speed up the building process. Didn’t feel the need to include use of a UI library at this time.&lt;/div&gt;&lt;/details&gt;&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;&lt;summary&gt;Hashnode GraphQL API’s&lt;/summary&gt;&lt;div data-type=&quot;detailsContent&quot;&gt;In order to show a section of my recent blog posts, I would need to fetch the data from the Hashnode GraphQL API’s. I picked graphql-request to handle fetching the data. It’s a simple and lightweight GraphQL client with TypeScript support. I could have skipped it altogether and just used the native &lt;code&gt;fetch&lt;/code&gt; but I like opting for this when working with GraphQL. It makes no significant difference to the bundle size here and I anticipate more data fetching in the future.&lt;/div&gt;&lt;/details&gt;&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;&lt;summary&gt;TypeScript&lt;/summary&gt;&lt;div data-type=&quot;detailsContent&quot;&gt;Astro is supplemented with TypeScript to provide type-checking. I would always opt for it over plain JavaScript where possible.&lt;/div&gt;&lt;/details&gt;&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;&lt;summary&gt;Cloudflare Pages&lt;/summary&gt;&lt;div data-type=&quot;detailsContent&quot;&gt;For deployment I chose Cloudflare Pages with auto deployments from Git. As with many hosting providers, it has simple git integration, previews, and a generous free tier.&lt;/div&gt;&lt;/details&gt;

## The approach

There weren’t any technical challenges with this project so the primary focus was on the UI/UX and delivering a fast and accessible experience.

### How was the Astro experience

This was my first project building with Astro. Overall it was a pleasant experience. The docs are great and adapting to Astro was straightforward. At first I was experimenting using React alongside Astro which is viable with the help of Astro client directives. But I quickly realised React is overkill here. I wanted to take advantage of astro components `.astro` as they render to static HTML without client-side runtime, and so I converted any straggling React into Astro files and removed the React dependencies altogether.

An example of this was a light/dark theme toggle. Initially I wrote this in React but it was simply wasn’t needed and therefore I refactored to plain TypeScript:

```typescript
// ThemeToggle.astro
---
import { Icon } from &apos;astro-icon/components&apos;;
---

&lt;script&gt;
	import { Themes, THEME_STORAGE_KEY } from &apos;../consts/index&apos;;
	import type { Theme } from &apos;../env&apos;;

	const button = document.getElementById(&apos;theme-toggle&apos;);

	const theme = (() =&gt; {
		if (typeof localStorage !== &apos;undefined&apos; &amp;&amp; localStorage.getItem(THEME_STORAGE_KEY)) {
			return (localStorage.getItem(THEME_STORAGE_KEY) as Theme) || Themes.light;
		}
		return window.matchMedia(&apos;(prefers-color-scheme: dark)&apos;).matches ? Themes.dark : Themes.light;
	})();

	const reflectThemePreference = (theme: Theme) =&gt; {
		document.documentElement.className = &apos;&apos;;
		document.documentElement.classList.add(theme);
		button?.setAttribute(&apos;aria-label&apos;, `${theme} theme`);
	};

	reflectThemePreference(theme);
	window.localStorage.setItem(THEME_STORAGE_KEY, theme);

	const handleToggleClick = () =&gt; {
		const element = document.documentElement;
		const newTheme = element.classList.contains(&apos;dark&apos;) ? Themes.light : Themes.dark;
		reflectThemePreference(newTheme);
		localStorage.setItem(THEME_STORAGE_KEY, newTheme);
	};

	button?.addEventListener(&apos;click&apos;, handleToggleClick);
&lt;/script&gt;

&lt;button
	id=&quot;theme-toggle&quot;
	title=&quot;Toggles light &amp; dark theme&quot;
	class=&quot;group flex items-center justify-center rounded-full p-1.5&quot;
&gt;
	&lt;span
		class=&quot;duration-400 block text-zinc-900 transition-transform ease-in-out group-hover:-rotate-90 dark:text-zinc-50&quot;
	&gt;
		&lt;Icon name=&quot;sun&quot; class=&quot;block dark:hidden&quot; width=&quot;24&quot; height=&quot;24&quot; /&gt;
		&lt;Icon name=&quot;moon&quot; class=&quot;hidden dark:block&quot; width=&quot;24&quot; height=&quot;24&quot; /&gt;
	&lt;/span&gt;
&lt;/button&gt;
```

Removing the React code and dependencies helped reduce the bundle size and ensured I was taking advantage of what Astro offers.

I’m sure as the project grows there will opportunities to explore some of the latest Astro features like Astro Server Islands architecture.

### Design overview

Design is usually the tricky part for me. It’s easy to start experimenting too early and lose time to ideas that didn’t work out. This time I was determined to keep things simple at first and then slowly add extra layers over time.

Because I was not attempting complex design from the start, I decided to skip pre-designing the page in Figma. This is something I have done in the past for small side projects and it helped. But on this occasion I wanted to get my ideas into code as soon as possible.

### **Every site needs a** `button`

One of the earliest things I did was create a simple `button` component with a couple variants. To add some polish I utilised CSS transitions and icon slots. The button is one of the first things you notice on a site, and usually what you click first so I wanted to design a nice version.

### **Navbar UX**

Recently I wrote an article on a specific type of UX for a main site navigation and I decided to opt for this behavior on my portfolio. You can find a tutorial on this with more information here:

[5 tips to level up your React codebase](/blog/5-tips-to-level-up-your-react-codebase)

### **Color scheme**

It’s easy to find a good color scheme. There are countless free tools to help you pick a pre-defined theme. I wanted my primary theme colors to be simple white/dark/gray to keep a clean and modern look. On top of that I added some accent colors to the tailwind config that added a bit more life to the site. They would help bring some ‘pop’ to elements in areas such as focus styling, icons, and background sections.

```javascript
	&apos;accent-darkest&apos;: &apos;#6F61C0&apos;,
	&apos;accent-dark&apos;: &apos;#A084E8&apos;,
	&apos;accent-bright&apos;: &apos;#8BE8E5&apos;,
	&apos;accent-brightest&apos;: &apos;#D5FFE4&apos;,
```

### Icons

As far as Icons go, I opted to keep a local copy of icons I would use in the repo using `/src/icons`, saving each icon as a `.svg`. There is a package called [astro-icon](&lt;https://www.astroicon.dev/)&gt;) that helps simplify working with icons in Astro projects, this handles embedding the icon.

As far as the icons themselves I opted for icons from the open source project [iconoir](https://iconoir.com/). Largely because of the simple one click copy of the svg code and the large free set of icons available.

If I ever decide to update icons in the future, changing one icon at a time can be easily done.

---

Once those design decisions were made, most of the blockers to writing up core code was done and I could proceed with the build.

## Outcome

### What went well

Overall I am happy with the outcome. The site feels fast thanks to the lack of JavaScript and server first approach of Astro. It scores well across Lighthouse scores, which admittedly should be the minimum standard for a site like this.

There was some trial and error with the design but I didn’t get stuck iterating on anything for too long. There’s always room for iteration and improvement. I am not a designer but I was able to harness some inspiration and tips from my experience working with a talented design team. An example is that I paid closer attention to text colors/weights than I would have previously, layering in different shades &amp; weights where appropriate.

### What would I do differently next time

I would have avoided beginning the project with React and Astro, and just started with Astro. I had written some components in React initially but soon realised it wasn’t necessary. Likely that since I had been working with React for so long, it was strange to separate initially.

### What can be improved

&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;&lt;summary&gt;Image handling&lt;/summary&gt;&lt;div data-type=&quot;detailsContent&quot;&gt;I will consider moving the images used to a suitable CDN as the project and number of images grows. Currently the images are stored locally alongside the code in &lt;code&gt;src/images&lt;/code&gt; where Astro optimises and bundles them. There is likely further optimisation that could be done here.&lt;/div&gt;&lt;/details&gt;&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;&lt;summary&gt;Content&lt;/summary&gt;&lt;div data-type=&quot;detailsContent&quot;&gt;I intend to improve upon the content of the site as a main priority. I will be working on some small scale side projects that help make my life easier and that serve as good highlights of my current skills to flesh out the projects section. This section is lacking currently but I’m not interested in filling it out with outdated projects that don’t represent my skills anymore. I will also be writing articles consistently again to add more recent content to the blog.&lt;/div&gt;&lt;/details&gt;&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;&lt;summary&gt;Automate showing new articles&lt;/summary&gt;&lt;div data-type=&quot;detailsContent&quot;&gt;Making sure new articles show up without having to do manual cache purging.&lt;/div&gt;&lt;/details&gt;

There are further minor improvements I intend to make to various sections as well.

## Summary

I recently built a new personal developer portfolio for myself using Astro, TailwindCSS, and TypeScript ensuring a fast, accessible site with great SEO. The project focused on simplicity, speed, and clean design and the case study helped me contextualise the project and seek ways to improve. It was a lot of fun to build and I’m excited about all the things I can add and improve upon in the future.

Thanks for reading!</content:encoded></item><item><title>cvrsnap.com: Blog post cover image creator to help you publish quicker</title><link>https://kieranroberts.dev/blog/cvrsnapcom-blog-post-cover-image-creator/</link><guid isPermaLink="true">https://kieranroberts.dev/blog/cvrsnapcom-blog-post-cover-image-creator/</guid><description>Create custom blog post cover images quickly with CvrSnap. Save time designing and publish faster with this free online tool.</description><pubDate>Fri, 31 Jan 2025 08:04:26 GMT</pubDate><content:encoded>Introducing [cvrsnap.com](https://cvrsnap.com/) 🫰.

CvrSnap is a free tool I recently built as a small side project and it lets you quickly create blog post cover images that you can download as a PNG. It’s designed to help you build something that looks good quickly so you can spend less time designing, and more time publishing. You can customise foreground text and it’s layout, customise the background, choose provided download dimensions, and more. The editor persists your progress if you need take a pause. Behind the scenes CvrSnap is a 1 page client-side React app built using React Router v7, TypeScript, MantineUI, Zustand, IndexedDB, and SST to provision the services Amazon CloudFront, Amazon S3, and Amazon Route 53.

I wanted to share the tool with all of you in case it can be of use to you, and also write a little case study for the project. If you are in need of a nice cover image for your next blog post, please do try it out and let me know if there’s anything else you would want to make your ideal cover image as quickly as possible. You can reach out to me [@Kieran6Dev](https://x.com/Kieran6dev), [on LinkedIn](https://www.linkedin.com/in/kieran6roberts/), or through the comments here.

## Why did I build this tool?

A blog post cover image is something I need every time I write a new post. I don’t have a current template designed (somewhere like Figma) for me to re-use across my posts and I don’t want to spend the time designing one because I always find it difficult to settle on something when starting from nothing. I just want to visit some domain, click a few buttons and be done with it. You might ask:

&gt; But wouldn’t it take longer to build the app than to design a new cover template in Figma

Yes. But I am a developer and I thought this would be more fun. Plus it might be useful to someone else.

I don’t care about elaborate blog cover images for my own posts. As long as the title is in the image, and maybe my name as the author. Generic cover images from the internet (Unsplash as an example) are also something I don’t care for. With that in mind I set out to build a simple, clean editing user interface where I could go anytime I needed a new blog cover image.

## Who is the tool for?

- Someone who needs a good looking blog cover image with text as the primary foreground, usually for the blog title and author (you can remove the text if you so choose).
- You don&apos;t want a generic cover image from an internet image platform.
- You don&apos;t want to spend hours starting from scratch using a design tool. You just want to pick some preset templates, maybe change some font settings etc.
- You want to do all of this in a modern and user-friendly editor.

CvrSnap is also **free**.

## The live site

[cvrsnap.com](https://cvrsnap.com/)

[codebase](https://github.com/kieran6roberts/cvrsnap) - (If you like and use the app, take a second to give it a star on GitHub)

![CvrSnap editor page screenshot](https://cdn.kieranroberts.dev/blog/cvrsnap-com-blog-post-cover-image-creator-to-help-you-publish-quicker-3.webp)

## CvrSnap example covers

Here are some example covers images I quickly built using CvrSnap:

![CvrSnap example cover with a split color background and foreground text](https://cdn.kieranroberts.dev/blog/cvrsnap-com-blog-post-cover-image-creator-to-help-you-publish-quicker.webp)

![CvrSnap example cover with a pattern background and foreground text](https://cdn.kieranroberts.dev/blog/cvrsnap-com-blog-post-cover-image-creator-to-help-you-publish-quicker-2.webp)

## Requirements

1. A very simple landing page
2. Editing interface
3. Minimum set of customisation options to start with.

   1. Text placement, size, font, etc.
   2. Background colors, templates, images etc.

4. 1 click image download in PNG format that accurately resembles the preview.
5. An editor that would persist the selected design options.
6. Different download size options.
7. Light/dark theme toggle.

## Tech Stack

- [React Router v7](https://reactrouter.com/home)
- [TypeScript](https://www.typescriptlang.org/)
- [`html-to-image`](https://www.npmjs.com/package/html-to-image)
- [MantineUI](https://mantine.dev/)
- [SST](https://sst.dev/)
  - [Amazon CloudFront](https://aws.amazon.com/cloudfront/)
  - [Amazon S3](https://aws.amazon.com/s3/)
  - [Amazon Route 53](https://aws.amazon.com/route53/)

## Build Specifics

### React Router v7 &amp; the rendering approach

The backbone of the app is built using React Router v7. I started out development in an SSR approach using React Router as a framework. But during development I realised it would be better served as a single page client side application. There would be no sever side data fetching requirements from external API’s or databases. The key for a dashboard type app is to optimise for snappy UI and quick transitions. SSR has been pushed as a ‘default’ for a little while now but I don’t believe it’s necessary in all cases, and overkill (possibly a little detrimental) here.

The new version of React Router was a little confusing to me at first because you can decide to run it as full framework, or as a simple routing library. I think there’s work that can be done to better explain this product now it’s merged with [Remix](https://remix.run/). Currently CvrSnap is running using React Router as a framework with `SRR: false` in the React Router config to specify we intend to use a single page client rendering strategy. This is primarily due to the fact I started out the app in framework mode without settling on the preferred rendering strategy.

I will likely refactor to use React Router simply as a routing library instead of the framework option currently in use. I believe this is usually how you would adopt a single page with React Router as described [here](https://reactrouter.com/home#react-router-as-a-library). I’m currently putting together a small starter kit (not done) for the the tech behind CvrSnap and I have implemented React Router simply for routing there as a trial run which looks good.

Outside of how the client side rendering is approached here, I am happy with the decision to prefer it to SSR for now.

### The persistent editor: Zustand + IndexedDB

Before building the app, I knew I wanted to persist the editor state for the user. If the user navigates to a different page, or away from the app, when they come back they should see the same design they left behind.

It’s quite frustrating to use apps in this realm that don’t persist your progress. You make some changes, think you are done and finish up, then realise you actually want to adjust something.

&lt;div data-node-type=&quot;callout&quot;&gt;
&lt;div data-node-type=&quot;callout-emoji&quot;&gt;🤦&lt;/div&gt;
&lt;div data-node-type=&quot;callout-text&quot;&gt;Whoops, now you have to start all over again.&lt;/div&gt;
&lt;/div&gt;

CvrSnap does not have user authentication, and doesn’t need it. Also no external database storage beyond browser based storage. Since no sensitive data needs to be persisted, browser storage becomes a nice solution for the needs of the app. The data that does need to be persisted is simply non-sensitive arbitrary numbers and strings, corresponding to text content, font sizes etc.

#### IndexedDB

The core of the editor state is persisted using IndexedDB. IndexedDB is simply an API for client-side storage of a large amount of structured data. I preferred this option for storing the current cover preview state over `Window.localStorage` for a few key reasons:

1. I would be writing to the storage quite often (as cover settings are updated). Local storage updates are synchronous: blocking the main thread with reads and writes. Therefore performance would likely take a hit.
2. IndexedDB allows structured data which I would would be working with (objects) without requiring it to be serialised.
3. IndexedDB can store larger amounts of data, something which might become relevant as the app grows.

`Window.localStorage` is however utilised for a couple of small scale settings like the sidebar open state and sidebar open sections states.

All the current settings of the active cover image preview are saved using IndexedDB in combination with the state management tool Zustand.

&lt;div data-node-type=&quot;callout&quot;&gt;
&lt;div data-node-type=&quot;callout-emoji&quot;&gt;💡&lt;/div&gt;
&lt;div data-node-type=&quot;callout-text&quot;&gt;The only editor state that is not currently persisted are user uploaded images. Users can upload their own image to the background. I didn’t want to persist personal images mostly for privacy reasons.&lt;/div&gt;
&lt;/div&gt;

Zustand is simply a small, fast, barebones state management tool. When you navigate on the client between the `/` page and `/create` page, that is Zustand persisting your current editor state. I have integrated Zustand with the `persist` middleware from `zustand/middleware`, allowing you to store the Zustand state in a storage option of your choice. You can see this in effect after you perform the initial page load.

To demonstrate the approach, here is a simplified solution:

```javascript
import { create } from &apos;zustand&apos;;
import { get, set, del } from &apos;idb-keyval&apos;;
import { persist, createJSONStorage, StateStorage } from &apos;zustand/middleware&apos;;

// Interact with IndexedDB using the promise based keyval store &apos;idb-keyval&apos;
const indexDBStorage: StateStorage = {
  getItem: async (name: string): Promise&lt;string | null&gt; =&gt; {
    return (await get(name)) ?? null;
  },
  setItem: async (name: string, value: string): Promise&lt;void&gt; =&gt; {
    await set(name, value);
  },
  removeItem: async (name: string): Promise&lt;void&gt; =&gt; {
    await del(name);
  }
};


export const useStore = create(
  {
   hasHydrated: false,
   setHasHydrated: (state) =&gt; set({ hasHydrated: state }),
   // Rest of the state initialisation + update functions

  },
  {
      name: &apos;editor-storage&apos;,
      storage: createJSONStorage(() =&gt; indexDBStorage),
      // Pick the state you want to persist.
      partialize: (state) =&gt; ({
        template: state.template,
        primaryText: state.primaryText,
      }),
      // Updating version becomes useful when we have breaking changes
      version: 1,
      onRehydrateStorage: () =&gt; (state, error) =&gt; {
        if (error) {
          // do something
        } else if (state) {
          state.setHasHydrated(true);
        }
      }
    }
)
```

One thing to note about persisting the state in IndexedDB is that the operations are asynchronous. This means the store is hydrated from IndexedDB some time later after store creation (after initial render). Therefore, there is a slight delay in having this data available in the editor. When the storage has been successfully hydrated in the `onRehydrateStorage` function, we set the `hasHydrated` state to `true` and use it wherever it’s needed.

### Dom to image handling

I experimented with several different packages that allow me to capture a DOM node as an image and saw mixed results. I settled on [html-to-image](https://www.npmjs.com/package/html-to-image) for the time being. So far it seems to be the most accurate representation of the DOM I have, with minimal configuration Crucially I was also able to capture `clip-path` elements correctly using html-to-image, an alternative solution I also was experimenting with ([html2canvas](https://html2canvas.hertzen.com/)) did not support this. The background templates make use of the CSS property `clip-path` to create custom backgrounds so this was a dealbreaker. There’s still some optimisation and experimenting I want to do to optimise the final images.

The logic behind downloading the PNG is pretty simple. The PNG image blob is generated from the DOM with `htmlToImage.toBlob`, setting the intended dimensions, quality etc. Then the blob is downloaded on the client with the help of [file-saver](https://www.npmjs.com/package/file-saver): `fs.saveAs(blob, &apos;cvrsnap-cover.png&apos;)`

### MantineUI

I wasn’t looking to spend time writing custom UI elements and design specs for this app. Since there might be some very interactive UI also, I decided to integrate a UI component library. I read good things about the React component library [MantineUI](https://mantine.dev/) and decided it would be a good fit for the project to get things done quicker. It’s open source, TypeScript based, and adaptable to various modern Frameworks. The component list of it’s core library is extensive and I would need to utilise several different various complex element compositions in the app.

CSS modules is recommended alongside Mantine and that’s what I went for, using CSS variables where helpful. From someone who enjoys TailwindCSS as my primary tool, I was missing it a little here but it was also nice to write regular CSS again. Something which I have not done for a while.

### Infrastructure

The last piece of the puzzle is the infrastructure. It’s been a long time since I had to deploy a client side single page application and I was open to exploring different possibilities.

Recently I have been diving into learning and building in the cloud using AWS. [Amazon CloudFront](https://aws.amazon.com/cloudfront/), [Amazon S3](https://aws.amazon.com/s3/), and [Amazon Route 53](https://aws.amazon.com/route53/) are some of the services I have been exploring and I set on deploying the site this way. What are these services:

- **CloudFront** is a web service that speeds up distribution of your static and dynamic web content.
- **S3** is an object storage service where you can store data inside containers called buckets.
- **Route 53** is a highly available and scalable cloud domain name system (DNS) service.

While I had been experimenting and learning so far mostly using the AWS web interface, for CvrSnap I wanted to define the infrastructure as code (IaC). I briefly thought about using the [AWS Cloud Development Kit (AWS CDK)](https://docs.aws.amazon.com/cdk/v2/guide/home.html) before coming across the [Serverless Stack (SST](https://sst.dev/)) and deciding that would be a nice approach. SST is a framework where you define everything your full-stack app needs in code and SST abstracts away a significant amount of the infra configuration and provisioning. Even more so than the AWS CDK. I though this would help me get to production quicker as I am not yet proficient in provisioning resources using the CDK despite working in and around CDK apps a little, then I could explore the CDK at a later date.

This article on [AWSFundamentals](https://blog.awsfundamentals.com/) has a great section detailing IaC, AWS CDK, and the benefits of SST if you want more information: [blog.awsfundamentals.com/social-stats-dashboard-sst-nextjs#heading-serverless-stack-sst](https://blog.awsfundamentals.com/social-stats-dashboard-sst-nextjs#heading-serverless-stack-sst)

Infra provisioning for the static site is taken care of by SST during deployment using a few simple lines of code:

```javascript
// infra/web.ts
import { config } from &apos;../config&apos;;

export const frontend = new sst.aws.StaticSite(&apos;Frontend&apos;, {
	path: &apos;packages/frontend&apos;,
	build: {
		output: &apos;build/client&apos;,
		command: &apos;pnpm build&apos;,
	},
	indexPage: &apos;index.html&apos;,
	domain:
		$app.stage === &apos;production&apos;
			? {
					name: config.domain,
					redirects: [`www.${config.domain}`],
				}
			: undefined,
});
```

I have also taken the time to dive into what SST does behind the scenes when calling `sst.aws.StaticSite` during my learning sessions, exploring which resources get created and how they interact. For someone who is still relatively new to building in AWS, I found SST to be really neat. I could get to production much faster with infra guaranteed by SST, and spend time digging into the lower level details afterwards.

I’m excited to explore more possibilities if the app ever requires further resources such as functions, storage etc. If you want to explore SST yourself, this resource provided a nice practical walkthrough: [https://guide.sst.dev/](https://guide.sst.dev/)

Getting back to the underlying infra, the static build output built is stored in a general purpose S3 bucket. Public access to the bucket is blocked and only the CloudFront distribution has access to the bucket through origin access control settings. I got to really dive into S3 bucket permissions/policies and origin access control settings when I has having a problem with initial deployments. Turns out my build output path was incorrect 😄, but at least I got to dig down into these topics. Finally after configuring Route 53 DNS service for `cvrsnap.com` , traffic could then be routed to the domain using the CloudFront distribution.

There are multiple guides out there that goes into depth on similar infra step-by-step, search for ‘AWS CloudFront S3 Route 53’ and you’ll find plenty of articles if you’re interested.

## What went well

### Product: Editor

I believe the app provides what I initially intended. Simple templates and options to get you moving quickly. Early on I was experimenting with a more custom approach, allowing dragging and resizing of text and an overall slightly more interactive editor. But I realised that didn’t fit my initial purpose for it not to be complicated and open ended with its options. It has pre-designed layouts and options by design. Data is persisted so the user shouldn’t get that feeling of losing something they started. It also feels fast and snappy which is key.

There is a decent amount of customisation options here, between the text and the backgrounds options and I’m happy that you can make covers that don’t all look completely identical. I’m excited to expand the options here (specifically with more complex templates) while keeping the simplicity of making something look good quickly.

### Personal: Learning

I took a lot of learning and enjoyment from this project. A first time user of React Router v7, MantineUI, and the whole SST setup, I was able to expand my knowledge significantly. I now have the experience to make decisions between stacks involving these tools in the future. It was a great help in letting me become comfortable working with S3 buckets and CloudFront distributions which fits in nicely with the upskilling I’m currently doing.

There is invaluable knowledge and experience you gain when building your own projects from scratch. From the product development side, infrastructure, backend, continuous integration (CI), analytics, testing etc etc.

## What can be improved

### Mobile UX

The app is mobile responsive, and while I just released a new update to improve the mobile UX, I think there are still improvements to make here. Things like scroll position when switching sections and adding a scroll top button are two things that come to mind. Previously you would have to scroll to the top to see the preview, but I recently shipped and update so you have access to a preview button wherever you are scrolled which helped the UX significantly there.

Finally the default cover image text size is a constant. So on mobile it is too large for the preview size. You can manually reduce it to look appropriate but it’s not ideal. I don’t really want to adjust the font size as the screen size changes but I need to do some thinking on this.

### Customisation options

There can always be more customisation options, as long as they don’t add design fatigue to the user since that would defeat the purpose of the app. I’m not intending to introduce drag/resizing or anything open ended like that. Things like:

- More complex background templates.
- Templates that have a placeholder where you can upload an avatar to the cover.
- Background gradients.

### Landing page

The current landing is a bare bones one without too much thought. The editor was the primary focus but now that’s taking shape, the landing page can get an upgrade.

- Improved hero section
- Updated editor image
- More sections demonstrating the app and cover image details

### React router: Library or Framework

To run the app as a ‘true’ client 1 pager, I will probably refactor the app to use React router only for the routing as explained above. This tech debt could have been avoided if I started out this way.

## Summary

CvrSnap is a free tool designed to help users create custom blog post cover images quickly and easily. Built as a client-side React app, CvrSnap leverages modern technologies like React Router v7, TypeScript, and AWS. The app&apos;s editor persistently saves user progress using IndexedDB and Zustand and PNG download is a simple click away.

If you made it this far, you are my hero. If you use CvrSnap to download an image you use in a blog post, please share the post with me on [LinkedIn](https://www.linkedin.com/in/kieran6roberts/), or [Bluesky](https://bsky.app/profile/kieran6dev.bsky.social) and I’d be happy to read/share.</content:encoded></item><item><title>How to setup path aliases for development with Next.js &amp; Jest</title><link>https://kieranroberts.dev/blog/how-to-setup-path-aliases-for-development-with-nextjs-and-jest/</link><guid isPermaLink="true">https://kieranroberts.dev/blog/how-to-setup-path-aliases-for-development-with-nextjs-and-jest/</guid><description>Learn how to setup path aliases for development with Next.js and Jest. This will help you keep your imports clean and organised.</description><pubDate>Fri, 16 Apr 2021 09:37:41 GMT</pubDate><content:encoded>## TLDR: Update (2025)

### Next.js

Configure the `baseUrl` in the `tsconfig/jsconfig` along with your desired paths:

```typescript
// tsconfig/jsconfig.json
{
  &quot;compilerOptions&quot;: {
    &quot;@/*&quot;: [&quot;./src/*&quot;]
  }
}
```

### Jest

Install `jest` and `jest-environment-jsdom`:

```bash
pnpm install -D jest jest-environment-jsdom
```

Generate a Jest configuration:

```bash
pnpm create jest@latest
```

Update the jest config file to use `next/jest`:

```typescript
import type { Config } from &apos;jest&apos;;
import nextJest from &apos;next/jest.js&apos;;

const createJestConfig = nextJest({
	dir: &apos;./&apos;,
});

const config: Config = {
	coverageProvider: &apos;v8&apos;,
	testEnvironment: &apos;jsdom&apos;,
};

export default createJestConfig(config);
```

Finally match the paths option from your `tsconfig/jsconfig` to the Jest `moduleNameMapper`:

```typescript
import type { Config } from &apos;jest&apos;;
import nextJest from &apos;next/jest.js&apos;;

const createJestConfig = nextJest({
	dir: &apos;./&apos;,
});

const config: Config = {
	coverageProvider: &apos;v8&apos;,
	testEnvironment: &apos;jsdom&apos;,
	moduleNameMapper: {
		// Your desired paths
		&apos;^@/src(.*)$&apos;: &apos;&lt;rootDir&gt;/src/$1&apos;,
	},
};

export default createJestConfig(config);
```

You can find the original article below for reference.

---

## Original article

I wanted to share how you can setup path aliases for your imports. Sometimes you end up with various levels of file nesting and this can get messy fast.

Fortunately for us both Next.js and Jest support adding path aliases so we don&apos;t end up with something like this:

`import Component from &quot;../../../Component/Component&quot;;`

For any medium to large react project you will have realized that due to the component nature of the library we end up doing a lot of importing. Let&apos;s find out how we can change the above into this:

`import Component from &quot;@/Component/Component&quot;;`

for both development in Next.js and for our tests using the test Framework Jest.

## Next.js

### Step 1 - Create a Next app

I&apos;ll start with a brand new project so you can see all the steps. For this I will create a new Next application using `npx create-next-app@latest` so we can get up and running quickly.

### Step 2 - Create some files to import

Next I&apos;m going to create a `src` folder which is where I normally store the apps components, hooks etc. Let&apos;s add some files to our `src` folder to simulate how a real project might look 👇 .

Here I have created the following files

- `Header.js` and `Button.js` components inside a `components` folder
- `__tests__` folder with our component test files
- `useStorage.js` hook inside a `hooks` folder
- `config.js` inside a `lib` folder

The contents of the files is not important. As long as you export something from your components which I will show you in a second.

### Step 3 - Specify our path aliases

First we need to create a `jsconfig.json` file (or `tsconfig.json` if you&apos;re using TypeScript) and setup our path aliases. My setup looks like this 👇.

```typescript
{
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;.&quot;,
    &quot;@/components&quot;: [&quot;src/components/*&quot;],
    &quot;@/hooks&quot;: [&quot;src/hooks/*&quot;],
    &quot;@/lib&quot;: [&quot;src/lib/*&quot;]
  }
}
```

Here we can specify a base url that we start our import from. Commonly you will see the root `.` specified.

Next we can specify path aliases for our paths to the different folder locations. `paths` is an object where we start with our path alias and assign it an array of paths. A common syntax of alias is in the format `&quot;@/fileName/&quot;` but you can use anything you&apos;d like.

Let&apos;s take our `components` alias as an example. We are saying we want to match the alias `&quot;@/components/&quot;` to the path `src/components/` folder.

### Step 4 - Checking that our aliases work

Now let&apos;s add some basic code to our components:

```typescript
// src/components/Button.js
export default function Button() {
  return &lt;button&gt;Click me&lt;/button&gt;
}
```

and the header is the same except it exports a `&lt;h2 /&gt;` tag 🙂. We can import them into other pages/components using the path aliases like this 👇.

```typescript
// src/pages/index.js
import Button from &apos;@/components/Button&apos;;
import Header from &apos;@/components/Header&apos;;
```

That&apos;s it! We have successfully setup path aliases for our imports in Next.js.

## Jest

### Step 1 - Install and configure the necessary files

I wanted to add this extra step for two reasons

1. You should be testing your apps
2. It&apos;s good to keep your imports consistent across development code and tests

First let&apos;s download the packages we need to work with Jest. I will install the following packages:

- `jest`
- `babel-jest`

`jest` if the primary package we need to install in order to use the Jest testing framework. While `babel-jest`will help transform our code so we can include things like ES modules `import` syntax in our test files.

We also have to configure a `.babelrc` file with the following setup:

```typescript
{
  &quot;presets&quot;: [&quot;next/babel&quot;]
}
```

What we are doing here is telling babel to use the custom preset for Next.js.

## Step 2 - Add our `jest.config.js`

Finally we can now get on to our aliases in Jest. We first need to create a `jest.config.js` file. There are a couple of options we will pass in here and it looks something like this.

```typescript
module.exports = {
	testPathIgnorePatterns: [&apos;&lt;rootDir&gt;/node_modules/&apos;, &apos;&lt;rootDir&gt;/.next/&apos;],
	moduleNameMapper: {
		&apos;^@/components(.*)$&apos;: &apos;&lt;rootDir&gt;/src/components/$1&apos;,
		&apos;^@/hooks(.*)$&apos;: &apos;&lt;rootDir&gt;/src/hooks/$1&apos;,
		&apos;^@/lib(.*)$&apos;: &apos;&lt;rootDir&gt;/src/lib/$1&apos;,
	},
	transform: {
		&apos;^.+\\.(js|jsx|ts|tsx)$&apos;: &apos;&lt;rootDir&gt;/node_modules/babel-jest&apos;,
	},
};
```

The first thing we do is use the `testPathIgonrePattern` which is defined as an array of strings that specify which paths we would like to skip when testing. Here we include the `node_modules` directory and the `.next` directory which contains our build files.

We don&apos;t want Jest looking in these directories for tests and we specify these paths starting at the root `&lt;rootDir&gt;`.

Next comes our path aliases. We need to use the `moduleNameMapper` property to map our aliases. It is a map of regular expressions and in this case we setup our three current aliases of `@/components` `@/hooks` and `@/lib`.

You can do this for any path you are likely to be importing regularly. If you wanted to move on to integration tests and import you pages into tests you could do the same for example `@/page`.

The order in which you setup these aliases could matter however. These patterns are checked by Jest from top to bottom therefore you have more specific rules you should include them first.

For more information check out the [Jest - Configuring Jest](https://jestjs.io/docs/configuration#modulenamemapper-objectstring-string--arraystring).

Now if we add an empty test block to one of our files and attempt the import with the alias we won&apos;t receive any errors after running out test script. Our imports are now working as expected in Jest ✔.

```typescript
// src/__tests__/Button.test.js
import Button from &apos;@/components/Button&apos;;

describe(&apos;Button&apos;, () =&gt; {
	it(&apos;should do something&apos;, () =&gt; {});
});
```

## Conclusion

Set up path aliases for cleaner imports in Next.js and Jest by configuring the `baseUrl` and `paths` in your `tsconfig/jsconfig.json`. For Jest, install necessary packages, configure Babel, and set up a `jest.config.js` file with `moduleNameMapper` to map aliases for consistent imports across development and tests.</content:encoded></item><item><title>5 Simple Tips/Good Practises to Level Up Your React Codebase</title><link>https://kieranroberts.dev/blog/5-tips-to-level-up-your-react-codebase/</link><guid isPermaLink="true">https://kieranroberts.dev/blog/5-tips-to-level-up-your-react-codebase/</guid><description>5 simple React tips and good practises: improve maintainability, reusability, performance, and project management effectively.</description><pubDate>Thu, 23 Jan 2025 14:06:40 GMT</pubDate><content:encoded>I have been working as a Software Engineer in React codebases for almost 4 years now. In that time I have worked closely with many Senior Engineers. Reviewing code, having my own code reviewed, and general collaboration. The way I write code and organise things as a result has evolved.

This article will share some tips and good practises that I employ to help make my code more maintainable, reusable, organised, and performant within a React project. Some of the tips are applicable outside of a React codebase as well. I love to learn so please share any tips you have also in the comments. Let me know if you have a better/different approach to something that’s mentioned.

Here are 5 tips/good practises to consider implementing in your React codebases.

## 1\. Use ‘dumb’ components

A dumb component is simply a component who’s purpose is to be presentational (UI). It separates presentation from logic. It might accept some props, and render some UI. Some of the benefits are:

- Better reusability
- Better predictability
- Better separation of concerns
- Simpler to maintain

Any smart logic (state etc.) can be moved to a wrapping container and any data the dumb component requires can be accepted through props.

### Before ‘dumb’ components

```typescript
// ❌ Avoid tying a simple presentational element to complex logic

interface BlogCardProps {
  title: string;
}

export const BlogCard = ({ title }: BlogCardProps) =&gt; {
  // Here we are tying the useUser hook to an otherwise simple presentational card
  const { user } = useUser();

  return (
    &lt;article&gt;
      &lt;h2&gt;{title}&lt;/h2&gt;
      &lt;span&gt;{user.name}&lt;/span&gt;
    &lt;/article&gt;
  )
};
```

Perhaps we will want to reuse `BlogCard` later but we might not interested in the user name for that instance. Depending on what `useUser` is doing, you may be making an API call you don’t need, or running some logic that adds unnecessary performance overhead. Rudimentary example I know but the idea is there.

### After ‘dumb’ components

```javascript
// ✅ Dumb Component (Better):

interface BlogCardProps {
  title: string;
  userName: string;
}

const BlogCard = ({ title, userName }: BlogCardProps) =&gt; (
  &lt;article&gt;
    &lt;h2&gt;{blog.title}&lt;/h2&gt;
    &lt;span&gt;{userName}&lt;/span&gt;
  &lt;/article&gt;
);

// Blog Section becomes the smart container with complex logic
const BlogSection = ({ blog }: { blog: Blog }) =&gt; {
  const { user } = useUser();
  const { posts } = useBlogs();

  return (
    &lt;div&gt;
      &lt;section&gt;
       {posts.map((post) =&gt; {
         return (
           &lt;BlogCard
             key={post.id}
             blogTitle={blog.title}
             userName={user?.name ?? &apos;Unknown&apos;}
           /&gt;
          )
        })}
      &lt;/section&gt;
      ...
    &lt;/div&gt;
  )
};
```

A better approach is to designate a container component to handle the complex logic, and pass down the data to any presentational components that need it.

Dumb components are some my favourite components and I recommend you keep this in mind the next time you are composing some UI. It makes reusing and testing so much easier.

## 2\. `useState` isn’t always the solution

The `useState` hook is often overused. Many times we can perform the desired action with an alternative solution that improves the user experience.

A common case where `useState` is sometimes seen but not ideal is a component that does sorting/filtering.

### Before with `useState`

```typescript
// ❌ Non ideal approach with useState:

import { useState } from &apos;react&apos;;

const sortTypes = {
  popular: &apos;popular&apos;,
  newest: &apos;newest&apos;
} as const;

type SortType = (typeof sortTypes)[keyof typeof sortTypes];

export const BlogList = () =&gt; {
  const [sortType, setSortType] = useState&lt;SortType&gt;(sortTypes.popular);

  return (
    &lt;div&gt;
      &lt;select value={sortType} onChange={(e) =&gt; setSortType(e.target.value as SortType)}&gt;
        &lt;option value={sortTypes.popular}&gt;{sortTypes.popular}&lt;/option&gt;
        &lt;option value={sortTypes.newest}&gt;{sortTypes.newest}&lt;/option&gt;
      &lt;/select&gt;
    &lt;/div&gt;
  );
};
```

The UX of this example is poor for a few different reasons:

1. If we refresh, we lose our current sort and the default sort is restored. This can be frustrating to users who want to continue from where they previously were.
2. We are unable to share a link that includes a specific sort type.

We can implement a much more user friendly approach by utilising the URL as state.

### After using the URL as our ‘state’

```typescript
// ✅ Better approach using the URL:

import { useSearchParams } from &apos;react-router&apos;;

const sortTypes = {
  popular: &apos;popular&apos;,
  newest: &apos;newest&apos;
} as const;

export const BlogList = () =&gt; {
  const [searchParams, setSearchParams] = useSearchParams();

  const sortParam = searchParams.get(&apos;sortType&apos;);

  const sortType =
    sortParam &amp;&amp; sortParam in sortTypes ? sortTypes[sortParam as keyof typeof sortTypes] : sortTypes.popular;

  const onSortTypeChange = (e: React.ChangeEvent&lt;HTMLSelectElement&gt;) =&gt; {
    setSearchParams((params) =&gt; {
      params.set(&apos;sortType&apos;, e.target.value);
      return params;
    });
  };

  return (
    &lt;div&gt;
      &lt;select value={sortType} onChange={onSortTypeChange}&gt;
        &lt;option value={sortTypes.popular}&gt;{sortTypes.popular}&lt;/option&gt;
        &lt;option value={sortTypes.newest}&gt;{sortTypes.newest}&lt;/option&gt;
      &lt;/select&gt;
    &lt;/div&gt;
  );
};
```

Now our selected state is saved by the url, even after refresh. The user can save/share/bookmark the url and keep the current sort. We fallback to a default sort type `popular` if the url is not what we expect so the app works as expected. This also now incorporates the browser history allowing users to flick between sorts.

More features that can often make use of the URL as state is

- Filtering/sorting
- Pagination
- Tab interfaces
- Search functionality
- Multi stage forms

Keep this in mind the next time your writing your next `useState` hook.

## 3\. Think about how you organisation/structure your code repository

This one isn’t strictly React focused, but it applies nonetheless. Repository structure and good organisation is something we all was unsure of at some point. It’s not really something that’s taught akin to learning your chosen coding language. But as you see when you start working in real projects, the structure of the code has real affects on working within it successfully. This is mostly aimed at projects that start to grow beyond the simple few files. Most real React projects will have several components/pages/hooks/utils from early stages.

As your projects grow, you’ll want a structure that is:

- Scalable
- Maintainable
- Adheres to some good patterns

Ensuring these points are taken care of makes everything easier for you and any collaborators working on the project. Testing, debugging, working in parallel, you name it.

There is no ‘correct’ way of structuring your repository. Projects have different requirements and complexity.

Recently I have been enjoying implementing a ‘Feature-Based Architecture’ for my personal projects. As you may guess, it suggests structuring code by features e.g. `editor`, `preview`. I like the scoping here. Each feature directory may have it’s own `hooks`, `utils` etc. directories with code only that feature requires. A `shared` directory is used to include code that is shared across features.

### Example of a feature-based hierarchy

```markdown
src/
│
├── features/  
│ ├── editor/ # Editor feature
│ │ ├── components/
│ │ │ ├── Drawer.tsx
│ │ │ ├── TextSettings.tsx
│ │ │ └── BackgroundSettings.tsx
│ │ ├── hooks/
│ │ │ └── useEditorData.ts
│ │ ├── styles/
│ │ │ └── Drawer.module.css
│ │ └── index.ts
│ ├── preview/ # Preview feature
│ │ ├── components/
│ │ │ ├── CoverImage.tsx
│ │ │ ├── CoverImageControls.tsx
│ │ ├── hooks/
│ │ │ └── usePreviewData.ts
│ │ ├── styles/
│ │ │ └── CoverImage.module.css
│ │ │ └── CoverImageControls.module.css
│ │ ├── consts/
│ │ │ └── index.ts
│ │ └── index.ts
│ │
│ └── ... (other features)
│
├── shared/ # Shared/common code
│ ├── components/ # Reusable components
│ │ ├── Button.tsx
│ │ ├── Modal.tsx
│ ├── hooks/ # Shared custom hooks
│ │ └── useImageDownload.ts
│ ├── styles/ # Global or shared styles
│ │ └── global.css
│ ├── layouts/ # Shared layouts
│ │ └── navbar.tsx
│ ├── utils/ # General-purpose utilities
│ │ └── logger.ts
├── pages/ # However you app handles pages etc.
│ ├── index.tsx
```

There’s a nice encapsulation about this structure that I find appealing. Despite some of the code being a bit more ‘spread out’ that it would in some structures, I know exactly where everything should be when I create something new and I’m never second guessing or wasting time thinking about it. I hate not knowing where I should place a component/value etc, and this way I have clear pattern.

I have built many projects that started out with a flat/technology based approach. When starting out this way you still need to make decisions about how you are going to organise sub-directories like components. Do you group by location, feature, or something else. You certainly have to group in some way or end up with a directory that’s impossible to navigate nicely. You could group by feature within components for example, but let’s say you want to re-work a feature, or add some upgrades. Now you have to traverse throughout all the parent directories to find what you need like `components`, `hooks`, `utils`, `stores` and whatever else the feature uses to make your changes. Of course you can make searching the repo easier by keyword searching, but still.

Instead, with the feature based approach I can navigate to my feature and get started. Maybe I have to look in shared but it will be easier to navigate this directory now that most of the logic is encapsulated within the feature directory.

### Example of the flat/technology based hierarchy

```markdown
src/
├── components/  
│ ├── Drawer.tsx
│ ├── TextSettings.tsx
│ ├── BackgroundSettings.tsx
│ ├── CoverImage.tsx
│ ├── CoverImageControls.tsx
├── layouts/ # Layout components
│ ├── Header.tsx
├── hooks/ # Custom React hooks
│ ├── useImageDownload.ts
├── styles/ # Global or modular styles
│ └── global.css
├── utils/ # General-purpose utilities and helpers
│ └── logger.ts
├── pages/ # However you app handles pages etc.
│ ├── index.ts/
```

Another type of organisational design system you may come across, especially in a React project is ‘Atomic Design’. This usually involves grouping code into categories named something like `atoms`, `molecules`, `organisms`, `templates` etc. `atoms` being the smallest elements like a base Button component, and scaling up from there. This structure can work well specifically for React UI components grouping in a project with a lot of components.

And as you may see, it is possible to combine ideas from these repository organisational structures. There are many patterns out there to read about. The idea being that you should read up on the pros/cons of them and implement what you think fits well for your use case.

## 4\. Use common shared `consts` for your non-changing values

Let’s say we have some content that can be one of several different types. One of these types we identify as `posts`. It’s a string and will always be `posts`. You might use it like this:

```typescript
return (
  &lt;Select defaultValue=&quot;posts&quot; onChange={onItemChange}&gt;
    ...
  &lt;/Select&gt;
  &lt;PostsSection hidden={value !== &quot;posts&quot;} /&gt;
)
```

Here we refer to a string `posts` more than once. Every time we use the string `posts` we increase the possibility of misspelling it and causing errors. This is easily avoidable if you use TypeScript, but there is another issue.

Let’s say we get a directive that we now have to change this to the string `articles`, for whatever reason. Now we have to change every instance where it’s in use. That becomes a major pain if it’s used across multiple files and directories without being referred to by a common `const`.

Instead we should refer to the string using a `const` variable like this e.g:

```typescript
// consts/index.ts

// Common practice is use snake_case naming convertion with capitals
export const POSTS_KEY = &apos;posts&apos;;
```

```typescript
import { POSTS_KEY } from &apos;@consts/index&apos;;

return (
  &lt;Select defaultValue={POSTS_KEY} onChange={onItemChange}&gt;
    ...
  &lt;/Select&gt;
  &lt;SomeOtherComponent hidden={value !== POSTS_KEY}&gt;
)
```

Now you have one source of truth. It is common practice to have a directory called `consts` or `constants` where you store values that need to be shared across files. If the `const` is only ever going to be required in one file, it is fine to define it only where it is needed, then move it to a central location later when re-use is needed.

## 5\. **Passing primitives as props beats whole objects**

Let’s say we have an object `blog` and this object has several key-value pairs. We have a dumb component that needs access to some of this data. Instead of passing the whole object as one React component prop `blog={blog}`, it is good to instead prefer passing the individual values required by the dumb component:

### What I wouldn’t recommend

```typescript
// ❌ What I wouldn’t recommend:

type Blog = {
  title: string;
  coverImage: string | null;
};

// BlogCard doesn&apos;t even use blog.coverImage
const BlogCard = ({ blog }: { blog: Blog }) =&gt; (
  &lt;article&gt;
    &lt;h2&gt;{blog.title}&lt;/h2&gt;
  &lt;/article&gt;
);
```

There are a few key reasons this approach is not usually preferred:

1. It has access to data it doesn’t need.
2. `BlogCard` could mutate the original `blog` object causing side effects. Recently while diving into my AWS learnings, I learned about the _‘Principle of least privilege’_ and it applies here. Essentially we don’t allow the inner component the chance to do more than it should be able to.
3. Can lead to more unpredictable rendering behaviour due to React’s shallow comparison of props: [React memo troubleshooting](https://react.dev/reference/react/memo#troubleshooting)
4. Is less declarative in general.
5. Might make testing more difficult because there’s more data than required.

### Better approach passing only the required props

```typescript
// ✅ Better Approach:

type Blog = {
  title: string;
  coverImage: string | null;
};

const BlogCard = ({ title }: { title: BlogCard[&apos;title&apos;] }) =&gt; (
  &lt;article&gt;
    &lt;h2&gt;{blog.title}&lt;/h2&gt;
  &lt;/article&gt;
);
```

Now the component is more declarative, we can see exactly what data it accepts. It doesn’t have access to the original blog object and if we wanted to memoize the component, we could skip re-rendering the `BlogCard` successfully when other fields other than `title` of the blog object change.

I would prefer this approach almost always even if I needed to pass several different fields. If you see the number of props your component needs is growing, it could be a sign you need to refactor your component into several different different components. Each with less responsibilities than before.

## Summary

This article shares five key tips for improving the maintainability, reusability, organisation, and performance of your React codebases. It covers the benefits of using &apos;dumb&apos; components as they’re simpler to maintain and re-use, optimising state management beyond `useState`, and suggests exploring effective code repository structures. It also encourages the use of shared constants for consistency and maintainability, and finally favouring the passing of primitive values over whole objects in component props.

There is a ton of things I didn’t cover in this article that could have been in, I might write another article on the topic. Let me know if you have any interesting tips or best practises that you like to employ in your React codebases. I’d love to hear them.

Until next time!</content:encoded></item></channel></rss>