I love writing, mostly because I love talking, but after 25 years of machine-gun mouth, my family are pretty tired of listening to it. So I write, because other people don’t know how annoying I am yet.
Medium is a great platform for doing just that, with an intuitive post builder. Without it, I’d probably be fooling around with markdown, writing HTML & CSS for every new post, or simply releasing stuff as long walls of text. Who’s got time for that? Not this guy.
So I decided to learn how to create my own post builder in React, just in case there’s a future where sheep rise up and take over Medium, and refuse to let any non-sheep write on the platform. Baaa’d news.
The process was relatively straightforward, and I’m going to detail it here. Read on if you want to find out more.
- Intermediate knowledge of React & React 16 features.
- The ability to download packages from npm.
- A computer.
- Too much time on your hands. Like me.
create-react-app to get started, which I installed with
npm i -g create-react-app. Then I ran
create-react-app post-builder to generate the project, and loaded it in the browser with
npm run start.
After that, I deleted the following files:
And the service worker stuff from
Then, I edited my App.js file until it looked like this:
styled-components for this project, which is a great library for writing
css-in-js. If you’re retracing what I did here, you might want to use it too. Download it by running
npm i styled-components. But you can also use regular css, or sass, or the blood of your enemies. It don’t matter.
The PostBuilder Component
<PostBuilder /> component was to be the main component. It would have a single state field,
items. This would be an array of objects, each representing a single item on the page, like a textarea, an image or a code block.
The component would then map over the
items array, and render a single
<Item /> component for each.
<PostBuilder /> in
src/components/PostBuilder/index.jsx, and after adding all the stuff above, it looked like the following image. So something would appear on the page later, I stuck an initial item in state too.
<Item /> wasn’t created yet, so
<PostBuilder /> was throwing an error. I decided to do that next.
uuid is a nifty npm package which generates a unique id when you call it.
The Item Component
Then came the
<Item /> component, responsible for rendering the default post item (a textarea) or some custom one (like an image or a code block). I created that in
src/components/Item/index.jsx, and it looked like this:
The component used some conditional logic to render the default element (a
type was falsy. If it was truthy, it would render some other element. I would define those later.
With these things done, I imported & added the
<PostBuilder /> component to
<App /> (and added some padding to the page). With the initial element in the
items array, I was left with a page like this:
Wow. Ship it.
Styling the Item Component
It was then I decided to dance with the devil and do some css, which was an absurd decision because you couldn’t even type into the thing yet. But hey ho. A
textarea deserves to feel good in its own skin.
If you’ve never worked with
styled-components before, it’s pretty easy. I created a
style.js file in the same folder as the
<Item /> component. The code looked like this:
The call to
styled.div returns a React component like any other. All children of that component then have those styles applied to them. I imported it into
<Item /> and replaced the top-level div with it.
Done. My text box was looking sexy, and already on Tinder. (Btw, by sexy, I mean completely invisible. Which is quite poignant, if you think about it, because beauty is in the eye of the beholder, and it is therefore important not to set arbitrary standards. Thanks.)
Focusing the Text Box
The last thing I wanted to do, at that point, was make the
<Item /> focus on creation, i.e. the caret would appear in the box without manually clicking on it. That would also give me a chance to try out the
useEffect hooks. After doing that,
<Item /> looked like this:
The new stuff was on lines
5created the initial ref. Refs provide a handle on specific DOM elements so that you can perform actions on them.
7–9told the browser to immediately focus the textarea.
15attached the textarea to that ref, giving access to the element in the DOM.
Adding and Updating Items
At that point, the
textarea wasn’t actually working. You couldn’t type into it, because there was no way to update the
value prop being passed to it. I also had no way to add new items to the page. So, I needed two functions: one to add new items to the
items state field, and one to update the content of a specific item. I put them both in
<PostBuilder />. Here’s what they looked like:
This is the
addItem function, which is responsible for sticking a new item on the page. It receives the type of the new item and the content and adds them both to state.
The call to
setItems uses the callback method of updating the state, so that it is always working with the most up-to-date version. Then it uses array spread syntax to add a new item to
items, and returns the new array, which then becomes the new state.
This is the
updateItem function, again using the callback method of updating the state. It receives the id of the item to update and the new content.
7, it gets the correct item by using the provided id to retrieve its index.
8, it uses array spread syntax to create a copy of the old state in an immutable way.
9, it updates the content of the specific entry by using the index.
Then, on line
10, it returns the new array, which then becomes the state.
Letting Items Update Themselves
updateItem function available, I passed it to the
<Item /> component so that it could be used.
And then in the
<Item /> component, I wired it up.
The new stuff here is the addition of
updateItem to the props being passed in, and setting the
onChange prop of the
After that, I tried typing in the
textarea and it worked! Fabulous.
Creating a new Textarea
When writing a post on Medium, hitting the enter key will take you to a new text section. I wanted to do that too, because Medium shouldn’t get to have all the fun.
The easiest way was to create a new item when the
onKeyPress event was fired from the textarea. I would grab it, check that the key was
Enter, and then create a new
item if it was.
I first added a
handleKeyPress function in
<PostBuilder />, and passed it down to
null as the type here because it is a falsy value, and therefore will result in a
textarea being rendered, because of the conditional statement in
To finish that off, I added
<Item />’s props list and passed it to the
textarea element through the
After that, I could create
textareas all day. And I did. Yeehaw.
Lastly, I decided to work on a single custom component: images. At that point, the
content property on each of the items was only text, because
textareas only need text. But theoretically, it could be anything. So, for images, I decided it would look something like this:
And using that knowledge, I created the
<Image /> component at
But I couldn’t render it yet. That was the next step.
The <Item /> component could only render
textareas or a
<p> tag at that point. In order to render custom components, I had to think of some way to map them to their specific type. A mapping table seemed fine. In
<Item />, outside of the component body, I wrote:
And then I replaced the
<p> tag in
<Item /> with what you see highlighted below:
So, if type was not falsy, the condition would not return a
textarea, and instead would retrieve the correct component from the mapping table. Of course, if you passed it a type that didn’t exist, the whole thing would blow up. But what’s life without a little danger?
I know I said “lastly” above, but it was a dirty lie. I still had to create some way for the user to choose which new item to add to the post. Well, that, or styling, but since styling is about as enjoyable as chronic flatulence, I decided to create
I exported the
componentMappings object from
<Item />, so that I could use it to dynamically generate the toolbar, i.e. each key in
componentMappings would spawn its own button.
src/components/Toolbar/index.jsx, I created the
<Toolbar /> component itself.
<Toolbar /> loops over each key in
componentMappings and creates a button for it. Using the
addItem function which will be passed from
<PostBuilder />, it attaches an
onClick event and adds a new item, passing the key which will be the
type of the component, and an empty object for
content. I added
<Toolbar /> to the bottom of
<PostBuilder /> and passed
addItem to it.
At this point, I became aware of some problems.
Firstly, trying to create an image would break the app. This was because the ref being used in
<Item /> only existed when the
textarea was rendered. Because I was rendering as custom component instead of a
textarea, the ref wasn’t getting defined, and therefore trying to call
focus on it was throwing an error.
It was an easy fix, though. In
<Item />, I changed the
useEffect hook to this:
But there was a second issue. How would I get the
alt attributes for the image? I didn’t know these values on creation. The user had no way to provide them when clicking the
image button on the toolbar.
This one was a little trickier. In the end I decided to create new components for providing each of the properties that custom items would need. I made an
<EditImage /> component, and rendered that in
<Image /> if the
alt attributes were missing or falsy.
That was all pretty straightforward.
<EditImage /> provided two simple inputs and a button for setting the
alt attributes. It would use an
updateItem function, not yet provided, pass those values back up to the
items array in
<Image /> would render
<EditImage /> if
alt were falsy.
I wasn’t sure if this was the best way to approach the problem, but it did have a nice side effect. The edit component could be used more than once. If a user wanted to change the
src attributes later, the addition of this kind of component would make that much easier. So I decided to stick with it.
<EditImage /> needed access to the
updateItem function, so I added it to the
componentMappings object in
And updated the
componentMappings function call to pass the parameter:
And then added it to the props list and
<EditImage /> jsx in
And finally, updated the
updateImageProperties function in
With all that complete, I was left with this:
And when I clicked the
And finally, when I added a
Hello, Google? Yes. Hire me.
The last thing to do was styling. This section doesn’t actually add anything to the functionality of the application, so you could skip it entirely if you’ve been following along. But, if you’re weird and troubled and enjoy css, then read on, soldier.
Page Margin & Heading
In Medium, the body of your post is centered on the screen with a margin to either side. I added this by creating a
style.js file at the root of the
src/ folder which exported a new styled component called
AppWrapper. It had the following styles:
Then I used it in
<App />, and added a header:
The page then looked like this:
Styling the Toolbar
In the same folder as the
<Toolbar /> component, I created a
style.js file with the following code:bb
Then I added it to
Which left me with this:
Styling <Image /> and <EditImage />
<EditImage /> is a direct child of
<Image />, I would style them both at once. In a
style.js file in the same folder as
<Image />, I wrote this monstrosity:
Then I added the wrapper to the
<Image /> component.
And added the
.create-img class to the
And finally, my page looked like this:
If you followed along with this, and you enjoyed it, then perhaps you would like some ideas for what to do next. Perhaps you wouldn’t, in which case feel free to email me and let me know. You could:
- Add more custom components, including a
- Make the
<Image />component less awkward (any src and alt will be accepted, even bad ones, so you could validation, or a button that reloads the edit component)
- Add a button to remove items
For the most part, creating the Post Builder was quite straightforward, but there are some things I might rethink in future.
- The mapping tables feel a little weak. There might be a better way to do these.
- The <EditImage /> component was a last minute solution to a problem I hadn’t foreseen. I’m certain there are more suitable alternatives.
I learned a lot doing this. It may not have been the most complex of tasks, but it allowed me to use many different features of React, and required a lot of thinking about how to make components generic enough (me think? very hard). I also liked the way the top-level
items data structure influenced the entire view, and therefore most of
<PostBuilder />’s children were relatively dumb. I like dumb things, cos they make me look better.
Thanks for reading.