I’m Chris, an adult man with nearly authentic human emotions. I don’t live on the internet, but I do occasionally tweet and write code. I also work, but not by choice.
Below is a series of learnings in my pursuit of programming for fun and profit. Hat tip to the invaluable sources credited in the entries.
forwardRef
The generic props parameter passed to forwardRef
is not enforced on the prop object 😱
type Props = {
shape: 'round' | 'square';
}
const Thing = forwardRef<HTMLDivElement, Props>((
{ shape = "anything!" }, // 🐞 This is allowed!?!?
ref
) => {
// ...
})
This occurs because the desctuctured props object satisfies and extends the Props
type. A necessary feature of static typing that we’re not happy to see in this particular instance.
The solution is to explicitly type the props and ref params:
type Props = {
shape: 'round' | 'square';
}
const Thing = forwardRef((
{ shape = "anything!" }: Props, // ✅ Type error!
ref: ForwardedRef<HTMLDivElement>
) => {
// ...
})
<enter>
Forms will only naturally submit on <enter>
button if:
If you want to allow <enter>
to submit the form without a visible button then you can add a hidden submit button:
<input style= type="submit" />
Reference: https://www.tjvantoll.com/2013/01/01/enter-should-submit-forms-stop-messing-with-that/
<textarea>
You can easily resize <textarea>
elements based on their contents with a couple simple handlers.
const inputRef = useRef();
useLayoutEffect(() => {
const input = inputRef.current;
input.style.height = `${input.scrollHeight}px`;
}, []);
return (
<textarea
onChange={(e) => {
const input = e.currentTarget;
// Shrink to minimum height
input.style.height = `var(--minHeight)`;
// Grow to match content
input.style.height = `${input.scrollHeight}px`;
}}
ref={inputRef}
/>
);
You can specify the number and shape of variadic params in TypeScript using conditional typing:
type Things = {
'foo.event': never;
'bar.event': {
isEnabled: boolean;
}
}
function logThing<TEvent extends keyof Things>(
name: string,
...params: Things[TEvent] extends never
? [undefined?]
: [Things[TEvent]]
) {
// ...
}
This allows you to enforce both of these calls through the typechecker:
// Pass
logThing('foo.event')
logThing('bar.event', { isEnabled: true })
// Fail
logThing('foo.event', { other: false })
logThing('bar.event')
logThing('bar.event', { other: false })
The result is that we get a succinct function signature while maintaining full type safety.
TypeScript won’t correctly type object iteration through Object.keys
, but you can with Object.entries
.
for (const [k, v] of Object.entries(abc)) {
// ...
}
Reference: https://effectivetypescript.com/2020/05/26/iterate-objects/
<button>
autoFocusReact has special handling around the autoFocus
when applied to <button>
elements. Instead of using the HTML attribute, they call .focus()
on mount.
Reference: https://github.com/facebook/react/issues/11851
children
propsYou can reference React children
props from within the parent component (assuming 1 child):
function Foo({ children }) {
Children.only(children);
return cloneElement(children, {
onClick: (e) => {
// Do something smart
children.props.onClick?.(e);
},
})
}
You can apply a color gradient to text by using a few choice -webkit
properties:
h1 {
background: -webkit-linear-gradient(#eee, #333);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
Reference: https://css-tricks.com/snippets/css/gradient-text/
You can make mutually exclusive variants of a component’s props using TypeScript.
type CommonProps = {
className?: string;
}
type ExclusiveProps = {
onClick?: never;
url?: never;
}
type LinkVariantProps = CommonProps & Omit<ExclusiveProps, 'url'> {
url: string;
}
type ButtonVariantProps = CommonProps & Omit<ExclusiveProps, 'onClick'> {
onClick: () => void;
}
type Props = LinkVariantProps | ButtonVariantProps;
key
propFramer motion elements (e.g., <motion.div>
) require a key
prop even if they’re not rendered inside a list.
Reference: https://www.framer.com/docs/animate-presence/##unmount-animations
In order to make a non-button act like a button you need to add 4 things:
role="button"
tabIndex={0}
onClick={...}
onKeyDown={...}
The keydown
listener allows the pseudo button to be activated with specific keys just like regular buttons.
Here’s an example:
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.currentTarget.click();
}
}}
You can package this up into a helpful utility:
const buttonTriggerKeys = ['Enter', 'Space', ' '];
function getButtonRoleProps<TElement extends HTMLElement>(
onClick: (e: MouseEvent<TElement>) => void
) {
return {
onClick,
onKeyDown: (e: KeyboardEvent<TElement>) => {
if (buttonTriggerKeys.includes(e.key)) {
e.currentTarget.click();
}
},
role: 'button',
tabIndex: 0,
};
}
Reference: https://benfrain.com/converting-divs-into-accessible-pseudo-buttons/
You can focus one or more Jest tests by using test.only
. You can skip one or more tests by using test.skip
.
I came across a code snippet that lets you log every time a new element gains focus. Very helpful for debugging!
document.addEventListener('focusin', function() {
console.log('DEBUG: focus', document.activeElement)
}, true);
Reference: https://hidde.blog/console-logging-the-focused-element-as-it-changes/
Can be focused
opacity: 0
tabIndex={0}
Cannot be focused
display: none
visibility: hidden
tabIndex={-1}
Reference: https://fuzzbomb.github.io/accessibility-demos/visually-hidden-focus-test.html
You can separate flexbox/grid children using gap: 4px
instead of having to do div + div { margin-left: 4px }
. This removes the need to know class names or require specific positions of child elements.
gap
encompasses both row-gap
and column-gap
. Like margin, you can specify 1 value that applies to both or separate them into 2 values: gap: <row-gap> <column-gap>
.
Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/gap
All events in React bubble! Normally, events like focus
and blur
are local to the DOM element, but in React they bubble all the way up including through portals.
Since a portal can be anywhere in the DOM tree, it may seem that event propagation may occur separately. However, this is not the case. The portal retains its position in the React tree, regardless of its actual position in the DOM tree. This means that events fired in a portal will propagate upwards to ancestors in the containing React tree, even if it is somewhere else in the DOM tree.
Reference: https://jwwnz.medium.com/react-portals-and-event-bubbling-8df3e35ca3f1