div
and a CSS class, resulting in the ever wonderful:
<Wrapper>
<HeaderWrapper>
<p>Something</p>
</HeaderWrapper>
</Wrapper>
That renders into:
<div class="container">
<div class="header-container">
<p>Something</p>
</div>
</div>
This is a simplified example... what I'm trying to say is that my experience with components that make explicit use of children is bad. And I'm biased.
But when Felipe showed me his idea for a component that used children, not just for adding a wrapper, but for making decisions on which child to render based on parent props, I realized I should probably put my bias aside. And this is when we asked ourselves the question to end all questions:
How can I constraint the parent component's children type, to be of a single component type?
And thus, we set out on a mighty journey towards type enlightenment.
We started where every journey starts. Five steps further than we should have, by trying to immediately run something on a .tsx
file that looked like this:
interface ChildComponentProps {
a: number;
b: string;
}
interface ParentComponentProps {
children: React.ReactElement<ChildComponentProps>[];
}
const ChildComponent: React.FC<ChildComponentProps> = ({ a, b }) => (
<p>
{a} {b}
</p>
);
const ParentComponent: React.FC<ParentComponentProps> = ({ children }) => (
<>{children}</>
);
It seemed like we had triumphed! We had no red squiggly lines on our code and the idea looked sound. So, we tried it out:
const Usage = () => (
<ParentComponent>
<ChildComponent a={1} b="First Child" />
<ChildComponent a={2} b="Second Child" />
</ParentComponent>
);
This works fine. But we needed to make sure that Typescript would yell at us if we tried to give a child that wasn't a ChildComponent
. And we hit a concrete wall:
const Usage = () => (
<ParentComponent>
<ChildComponent a={1} b="First Child" />
<ChildComponent a={2} b="Second Child" />
<p>I'm not a ChildComponent, this shouldn't work</p>
</ParentComponent>
);
Narrator: It did work
There's a very simple reason why our component did not yell at us when we passed it a child that didn't fulfill the constrain we thought we had in place. And it has to do with the type of a FunctionComponent
in React.
Here we go:
FunctionComponent
is:
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
We're interested in the first line of that interface definition, the one where the function that takes props
is defined. So, we dive a bit deeper into what PropsWithChildren<P>
is and find this:
type PropsWithChildren<P> = P & { children?: ReactNode };
This is it. This is the aha moment. Or maybe it should've been, if we already knew how Typescript handles these cases, which we didn't at the time.
What we have here is a type extended by an intersection, where both sides of the intersection have differing definitions of a property with the same name. Remember, our P
in this case was:
interface ParentComponentProps {
children: React.ReactElement<ChildComponentProps>[];
}
See how both the P
and the inline type { children?: ReactNode}
have the children
property? And furthermore, they have different values!
So, how does Typescript resolve extended types where this happens? Well, it does the only thing that makes sense. It creates an union type out of them. What comes out after all this is done is:
interface FinalParentComponentProps {
children: React.Reactelement<ChildComponentProps>[] | ReactNode;
}
// This is ReactNode btw:
type ReactNode =
| ReactChild
| ReactFragment
| ReactPortal
| boolean
| null
| undefined;
// And this is ReactChild
type ReactChild = ReactElement | ReactText;
And that's it. ReactElement
is fulfilled by any JSX element, like our <div>Not correct component</div>
intruder up there. And this makes sense.
Apart from any internal React explanation (there is one, but now is not the place), at the type definitions perspective, this makes sense. React's component contract is that they will render the JSX passed into HTML. And HTML will let us pass <div>s
or anything else, inside anything really. Sure, sometimes it might yell at us for violating dom validations like a button inside a button, but it'll still let us do it. And so does React, letting us pass any JSX element as a child to any component that can take children. So, yeah, we learned that we can't do this at the type level. So, can we do it elsewhere?
Typescript can't do it. But this is JS, where everything is possible and the points don't matter. So, we can loop through the children and check their type. Then, blow everything up if it doesn't match what we wanted. Something like this:
const ParentComponent: React.FC<ParentComponentProps> = ({ children }) => {
children.forEach((child) => {
if (child.type !== ChildComponent) {
throw new Error("Only ChildComponents allowed!");
}
});
return <>{children}</>;
};
While this works... it's not ideal. We don't want our typed component to break at runtime because the person using it didn't know that it would break rules set in place by the framework itself. Let's not do that 😅.
There's another option to keep things typesafe and kind of get the end result we want... only it skips the usage of the children prop entirely. You probably already have an idea where I'm going with this:
interface ParentComponentProps {
childrenProps: Array<ChildComponentProps>;
}
const ParentComponent: React.FC<ParentComponentProps> = ({ childrenProps }) => {
return (
<>
{childrenProps.map((props) => (
<ChildComponent {...props} />
))}
</>
);
};
This way, our component will only render ChildComponents
and it will be typesafe at usage. But it bypasses the whole idea about using children
🙈.
There's a few other things that work. Instead of throwing an error, we could ignore that element and only render the ones that fulfill the type constraint. Or, we could assert on the existence of a prop in the child instead of the type, to keep it a bit less strict while also making sure that the children contain the data we need to render them correctly. There's a lot we can do... doesn't mean we should do it.
I still believe that children
are best reserved for libraries that concern themselves with wrapping components in order to enhance them. Think, CSS in JS, or stuff involving the Context api that wants to wrap things in Providers
.
Does it look cool to do stuff like this?
const Usage = () => (
<ParentComponent>
<ChildComponent a={1} b="First Child" />
<ChildComponent a={2} b="Second Child" />
</ParentComponent>
);
Sure it does. And it has its pros, like every child having their own children and making the ParentComponent
's api very flexible. But the cost for this, is runtime behaviour that will need to be explained in out of code documentation and maintained fresh in the minds of any developer using this component.
Given that writing good docs is one of the hardest tasks in software, I'd say that cost is too high for most cases.
]]>Shoutouts to Felipe who consistenly comes up with interesting ideas like this one. They usually end up in both of us learning a lot (and still disagreeing about the children property).
If you're anything like me, your personal website is a blog created by a Static Site Generator (SSG from now on) and it has gone through some identity crises. In my case, each of these moments has been marked by my decision to switch the technology powering the site. Here's how that went:
Each SSG provides focuses on different things. They also have different underlying technologies, so they'll naturally feel really different to use. Here's my personal experience with each of them:
My relationship with Hugo was pretty shallow, I must say. I don't write Go, so I found a theme online for a blog that seemed to fit my needs and went with it. I think the only customization I did was change the navigation items and create a category. It did its job, but having to learn a whole new programming language just to go deeper into customizing stuff was too big a price to pay for me, at that point in time, so I decided to switch to:
Ah, Gatsby. The day I started, I decided to go the simplest way I could think: find a blog theme I liked, install it and dump all my old content into the new system. After adding the theme, the build started failing. 30 minutes later, 5 different github issues and some of my own tinkering, I found out that one of the dependencies that the theme used needed to be updated to be compatible with the changes on another. Just another day in the JS office. Also, it didn't build with the node version I used by default (12 I think) and I needed to downgrade to v10 to build the site. As stubborn as I am, I'm also very lazy and when out of the box things don't work, well, out of the godforsaken box, I can't help but generate animosity towards the thing. It also happened with Create React App when I tried to use it on the one day they had published a breaking bug. Maybe I'm lucky like that.
Back go Gatsby. After all the issues had been ironed out, copying the content over and changing the frontmatter to fit the theme's structure was a bit easier. And then I decided to add pagination. Looking back at that commit, it seems rather straightforward, but boy oh boy how I suffered with it. It's partly my fault, for assuming tinkering with a premade theme would be easy. But the combination of the createPages
function and having to learn more Graphql turned what should've been a fun experience, into a grueling slog. And by the end of it, I was so put off that I stopped doing stuff on my page. Forever. Until last week.
Now, it's clear that Gatsby wasn't for me. It's probably more powerful than I needed it to be, but it's also true that there is a lot of complexity for very small output. I mean, I only wanted some HTML pages generated out of markdown, and pagination on the post list. For that I was using
And at the end of it, I had a website. Sure, it worked well, but it still wasn't getting me 100's on Lighthouse's performance report. The reason being that you still ship a javascript bundle whenever you use Gatsby, regardless of how static you make your website. The main reason for this, is to have the SPA like routing experience, where it seems like the browser never reloads. Don't get me wrong, that's nice and all, but what does it matter when I'm reloading for a simple HTML document?
Here's Eleventy's one promise:
Eleventy is a simpler static site generator.
The word simpler is important. You'll notice the distinct lack of buzzwords like blazing fast or modern. No, it's just a static site generator. And it's a simple one. And that, my dear reader, it is.
I went a different way with Eleventy. Because of how much I had already heard about it, I was beyond curious to dig in. I skimmed through the docs to get an idea of the concepts. And then, fate showed its hand: there was a huge twitter thread on how bad Gatsby (the company) was at treating their employees. A couple weeks before, Andy Bell had just released his course on Eleventy, Learn Eleventy From Scratch and ran a very timely sale to help people get off the Gatsby train. The path forward was pretty clear for me: buy and go through the course and swap out Gatsby for Eleventy when I was done. And so I did.
Doing stuff with Eleventy is a joyful experience. Most topics in Eleventy work as you expect them to. Collections, Layouts, Markdown, and Frontmatter are pretty straightforward, but there are a couple of things one needs to learn in order to elevate Eleventy to its full potential:
Once the pieces fall into place, Eleventy starts to shine. It really is simple in the best way possible. In the same time, it took me to add pagination to my Gatsby version, I managed to:
I attribute a big chunk of my success with Eleventy to Andy's amazing course (seriously, go buy it, it's great). But there is no mistaking the fact that the people behind Eleventy have done a marvelous job at creating a great developer experience.
Oh, and best of all, even though there's some Javascript powering some of the feature I built, other than the Service Worker register, the website contains no Javascript. Sure, it reloads with every click, but it takes a second on a Moto4G with a regular 3G connection to load. Who needs an SPA routing mechanism at that point?
I love Eleventy. It's rekindled my love of the web, HTML and CSS. Sure, I'm not learning the newest, greatest, most hype-worthy technology like I would if I invested in Gatsby... but maybe that's a good thing.
P.S: The only one negative thing about Eleventy is that using Typescript instead of plain JS isn't really straightforward. It's possible but needs some tinkering and the current methods make the experience a lot clunkier. Not a deal-breaker by any means, but it would be pretty awesome to have that out of the box.
]]>I’m really big on words and language. So, as usual, let’s see what the origin of the word can tell us about its meaning:
fact: From Latin factum (“a deed, act, exploit […])
The word fact comes from action, which explains a lot about why we now relate it to truth, or to something being real.
At this point in my research it dawned on me: this person wasn’t really talking about facts. He was talking about the value he gave to a specific fact.
To understand what comes next, we need to create some context. Let’s imagine a statement given by a totally fictional character. Let’s call him Emid:
Emid: I believe that people should work 100 hour weekdays.
Now, to me, that is a fact that in my mind proves that Emid is kind of evil. But, to another person, it might seem quite normal.
So, while I was assigning a value of evilness to that fact, the other person was valuing it much more mildly. And those different values that we all carry inside are what really matter.
If there were such things as irrefutable facts, we wouldn’t need judges. In fact, we’re all constantly imparting judgement to everything around us. That’s how we make sense of the world, how we build a reality. And we need such a reality, we need anchors in which to base our actions, or we would be lost, cast adrift in this sea of chaos that is life.
So, the next time you find yourself asking someone for facts that prove something, remember that what you’re really requesting are facts that align with your own personal values about something. I assure you, the world will be all the better for it.
]]>It’s been almost 2 months since I finished my journey in HolidayCheck’s apprenticeship program. And when I say finished, I mean it more in the sense that there was a time constraint from the start, but I don’t feel like anything has really finished. Not in the usual sense at least. The six months I spent as an apprentice were a beginning, if anything.
It may seem odd that I’m only now writing about finishing the apprenticeship process, two months after the fact, but I wanted to give myself some time for a real reflection. Time changes our perception of things, and when something is too fresh in memory, it might seem better (or worse) than it actually was. Today, that time has passed, and I feel ready to put my thoughts into words.
Let’s start at the end. My final week as an apprentice had a bit of turmoil surrounding it. The product team I was joining was going through a lot of changes, and it was upon me to make an important decision, one that would change the scope and experience of my apprenticeship. I had to choose between remaining 2 extra weeks as an apprentice, or joining the product team a bit earlier than originally planned. In a sense, this decision was what I had been preparing for these past six months. In the end, it was a matter of trust. I could trust our Scrum Master in that it was the best decision for the team, or I could trust myself that I could make it work regardless of when I joined the team. I decided to put my trust in others, as I had read so many times during the apprenticeship.
In retrospect, I’m convinced this was the right decision, but I’m also quite sure that before the Apprenticeship, I wouldn’t have been able to see it that way. All those days spent thinking and learning about collaboration and communication were paying off. And in doing so, everything about the Apprenticeship and its true value clicked inside my mind.
Turns out, how many hours I spent learning Haskell, or reading about Craft and XP are not as important as I initially thought they were. What really shows me I’ve grown is not what I did, but how I am acting now, after the fact. How I make decisions, both on the personal and technical level and how I communicate my intentions are the biggest consequence of all the time I spent learning about multiple topics. It’s not that I can now use VIM at a “bit better than beginner” level, or I can read and write simple Bash scripts, but how those experiences changed my view about technology, and about the work of a person that writes Software. Here’s a list of the things I now hold close to heart and mind:
All of these seem really obvious to me right now. But they didn’t 8 months ago, and I can only thank the time provided to me by the apprenticeship program for that.
The process of joining a product team after the end of my apprenticeship time was a really smooth one. I had a lot of major advantages when compared to any new team-member who hadn’t gone through such a process. There are the obvious ones: that I knew the people already, a bit of the codebase and a lot of the processes of the company. But there are also some hidden ones.
Starting something new is always challenging. There’s a lot of information coming and you still don’t have tools in place to filter what’s important and what can be safely ignored. Every new employee goes through a process of cognitive overload for the first couple months. In my case, I could extend that process to six months instead of 1 or 2, and that way I could keep my mind fresh and stress free throughout it. When I finally joined a team, I could focus 100% of my effort and energy into the things I knew were important for me now: the team dynamics and the actual code base. There was no noise in the background, because I had already dealt with all the noise beforehand. It was an enlightening experience, and by far, the best way to join a new team.
Like I said in the beginning, graduating from the Apprenticeship program feels more like a beginning than an end, mostly because I will remain involved in the program for the foreseeable future. I am now part of the team that’s working on it, and also quite excited for my upcoming role as a co-mentor to the new apprentices that will start arriving soon.
It feels like the challenge and learning opportunities of being in an environment where mentorship is important never really end. And I wouldn’t want it any other way.
]]>