How to build shared components

Background and types of shared code
You don't want to copy and paste the same chunk of code and this code chunk is used in more than 30 different places of the codebase. And you create a shared version of the code, which can be:
- A shared component. You can write it totally from scratch (with the help of 'TailwindCSS') or it's your tailored/customized way of using shared component library like 'Material-UI' or 'Shadcn' considering the app's own logics (and normally if you don't abstract a bit the usages of these libraries and use them directly in your codebase, things can be not that clean and convenient for the app's future developments, which can also be seen as the battle between your ideas of building and using things and the shared libraries' and is very common).
- A shared utility. You can write your own version of 'loadash' or more practically, you have your own ways of how to massage and display certain data like a post's properties (whether to show excerpt or not ) or time and currency format of the app. Many client/SDK code might belong to this category as well, e.g. 'Contentful' (CMS), 'prisma' (database), 'i18next' (localization), etc.
- Many other things, you name it...
Shared components and their API building
One typical work of building a shared component is to write its APIs, for example onClick and disabled of a shared Button component, but how about things like icon or even (very) specific/rare usages that might need to be added to support certain future development tasks (besides adding APIs, you can create new shared components like UserAvatar and AppAvatar based on the basic Avatar and these three are all shared components). Here we have several things to discuss:
- Can you predict all APIs of a shared component at the very beginning when you are just going to build it? If not (and I guess it's quite normal that a teammate might need things you don't think of when using a shared component which is built by you), all APIs that will be built after this component is already used in the codebase should be optional of you'll need to find all the existing implementations and add the new APIs, especially considering it's in more than 30 different places 😱. And so far my understanding is that it's a good practice to create a table with all possible APIs from your previous experience, e.g.
onChangefor input components,isOpenfor modal and menu components,variantClassnamefor components that need extra styles, etc. Every time you build a new shared component, you check the table and update the table, which for me can also be seen as the growth of an engineer. You always know which nail is for which material(s) or task(s) as a carpenter. - Besides adding new APIs, how about changing existing ones? I think
dateStringis definitely not enough forFormattedDateshared component and I need bothtimestamp(don't forget to considerthe year 2038 problem) andtimezoneOffset(most probably the only good format to work with time). Do I addtimestampandtimezoneOffsetand keepdateString, meaning only the new usages ofFormattedDateimplementtimestampandtimezoneOffsetand the old usages aren't going to be touched? Or addingtimestampandtimezoneOffset, deletingdateStringand modifying all existing implementations ofFormattedDateis better? Is this example quite 'stupid' and everybody knows to choosetimestampplustimezoneOffsetevery time when working with time and the above 'need to change' scenario just will never happen? Or I just think too much to make things too perfect according to my understanding about the shared APIs building here? - You can use
onClick(as a certain norm?) and you can also usetoClickorhandleClickor even justclick. Is it good to use more specificonButtonClickoronIconButtonClickor this makes the code too complicated and hard to maintain? Naming is also a very important thing to consider when building shared components, which can also be included in the table I mentioned above. - Special cases like 'Next.js' client and server components. You might have
LinkServerandLinkClientbuilt fromLinkBaseto support different usages in client or sever environment. This is because of the framework's functionality that you need new ideas of your shared component building. Similar scenarios might happen because of other functionalities or frameworks. - Similar discussions can go on and on and on...
Tutorials and over-abstraction
In order to solve the above questions besides just having your own ideas and practices, you can find many ideas/methods/ideologies of how to build shared code:
- This one is about building good component API and alsothis.
- If you work with 'TailwindCSS' specifically, people have discussions aboutmanaging (too) long classnames and 'TailwindCSS' itself hasinstructions about dynamic classnames.'TailwindCSS' really simplified the process of building shared components, saving a lot time and effort, thus it makes it more possible not to use any component library. But a side question might be whether coding things that are already there is good? Though you can always learn more in depth about the world by building existing things yourself.
- These two 'Youtube' videos are also good: one is named 'Minimal API Surface Area' bySebastian Markbage and the other 'The how's and why's of flexible React components' by Jenn Creighton.
- Many, many other tutorials, posts, articles, videos...
A very impressive article I read about shared components is from Dan Abramov and the story is basically like he spent a whole night refactoring his colleague's code that definitely had some big chunks of the 'repeated' code, felt very good about the achievement and the next morning knew that the sameness was for really useful special purposes that he totally neglected, through which he introduced the idea of over-abstraction to me.
However, I mean, there're thousands of ideas of how to build good shared components with their solid APIs and you have your own ideas (also considering business logics), which can be seen as a ever-lasting battle between you and these tutorials. This might be the very core of the overall shared code building issues from my understanding so far.
Conclusion
Besides the specific things we can do and questions we need to think of from the above ideas of this article and other tutorials, from the'Minimal API Surface Area' I mentioned above:
This process fundamentally has no end, ever. We are simply never going to realize a state of software nirvana where everything is supremely satisfying. That’s an important emotional realization.
Is this true? Especially during this age of AI.