Beginner End-to-End Tutorial: Building Minitwitter

This tutorial will show building a simple app, Minitwitter, end-to-end.

As you learn Plasmic, please let us know all your questions and feedback! Reach us on our Slack community or at team@plasmic.app.

About Minitwitter

Minitwitter is a very basic web app—its main page is just a wall of posts. There’s also a second page where anyone can enter a new post. There’s no login/auth or anything else. So it’s a great way to get started learning about all the core Plasmic concepts and workflow quickly, without getting bogged down in complexity that’s specific to any particular app or domain. It’s simpler even than the TodoMVC tutorial!

You can play with a finished version of the codebase here:

About this tutorial

This tutorial consists of two parts—design and code. If you want to just focus on the code, you can skip the design portion, and instead clone this completed Plasmic project (however, we still recommend the video tutorial as a primer to the main concepts in Plasmic, such as slots and variants).

https://studio.plasmic.app/projects/gT4hmyT4Gfq9iSDYjENowj

In the design section, you will learn the following concepts in Plasmic Studio:

  • Free-form drawing and wireframing
  • Artboards
  • Components
  • Layout
  • Slots
  • Variants, including interaction variants
  • Live preview mode

In the code section, you will:

  • Import Plasmic components into a React codebase.
  • Wire props up to the components, integrating with real React state.
  • Control what interfaces the components export.
  • Add multiple pages and routing.
  • Load and store data into real data stores.

You can find a completed version of the codebase here if you wish to refer to it:

https://github.com/plasmicapp/minitwitter

Design Minitwitter

Let’s begin with a video showing how to use Plasmic Studio to design the Minitwitter app, learning all the core concepts in Plasmic as we go:

Again, if you wish to skip this, you can clone the completed design project for the following code sections of the tutorial:

https://studio.plasmic.app/projects/gT4hmyT4Gfq9iSDYjENowj

Import the design into code

Create a new React app using create-react-app. In this tutorial we’ll be showing Typescript, but Plasmic also works with plain JS projects.

npx create-react-app minitwitter --typescript
cd minitwitter/
yarn start

If you click the Codegen button in the top toolbar, you’ll see instructions on how to get started importing into your codebase. Plasmic provides a CLI to make this into just a few simple steps.

First, install the CLI:

yarn global add @plasmicapp/cli
# or: npm install -g @plasmicapp/cli

Next, initialize Plasmic in your codebase. The first time you run this, you’ll be prompted to enter some credentials info (which is shown in that modal). Then you’ll be guided through a series of questions about how you want to codegen to work.

plasmic init

Finally, sync down the components:

plasmic sync -p Xx4hmyT4Gfq9iSDYjENowj

You now have a src/components/ directory, and a src/components/plasmic/ directory inside that.

Just render the static design in our app:

import React from 'react';
import Feed from "./components/Feed";

function App() {
  return <Feed/>;}

export default App;

Add the following to your index.css, which will ensure that the root component is mounted and sized correctly in the context of your React app.

#root {
  /* relies on align-items:stretch */
  min-height: 100vh;
  display: flex;
}

#root > * {
  /* Plasmic root element probably has height:100%, so clear that */
  height: auto;
}

Now if you check your running React app, you should see the

First glance at Plasmic components

Let’s look inside the Feed component:

// This is a skeleton starter React component generated by Plasmic.
// This file is owned by you, feel free to edit as you see fit.
import * as React from "react";
import { PlasmicFeed, DefaultFeedProps } from "./plasmic/minitwitter/PlasmicFeed";
// Your component props start with props for variants and slots you defined
// in Plasmic, but you can add more here, like event handlers that you can
// attach to named nodes in your component.
//
// If you don't want to expose certain variants or slots as a prop, you can use
// Omit to hide them:
//
// interface FeedProps extends Omit<DefaultFeedProps, "hideProps1"|"hideProp2"> {
//   // etc.
// }
//
// You can also stop extending from DefaultFeedProps altogether and have
// total control over the props for your component.
interface FeedProps extends DefaultFeedProps {
  children?: never;
}

function Feed(props: FeedProps) {
  // Use PlasmicFeed to render this component as it was
  // designed in Plasmic, by activating the appropriate variants,
  // attaching the appropriate event handlers, etc.  You
  // can also install whatever React hooks you need here to manage state or
  // fetch data.
  //
  // Props you can pass into PlasmicFeed are:
  // 1. Variants you want to activate,
  // 2. Contents for slots you want to fill,
  // 3. Overrides for any named node in the component to attach behavior and data,
  // 4. Props to set on the root node.
  //
  // By default, we are just piping all FeedProps here, but feel free
  // to do whatever works for you.
  return <PlasmicFeed {...props} />;
}

export default Feed;

If you clear out the comments, the code is quite minimal:

import * as React from "react";
import { PlasmicFeed, DefaultFeedProps } from "./plasmic/minitwitter/PlasmicFeed";
interface FeedProps extends DefaultFeedProps {
  children?: never;
}

function Feed(props: FeedProps) {
  return <PlasmicFeed {...props}  />;
}

export default Feed;

This is showing the “blackbox library” codegen scheme, the default scheme. In it, Plasmic generates a library of dumb presentational components for you—PlasmicFeed, PlasmicNewPost, PlasmicButton, etc. These (along with the corresponding CSS and asset files they import) are owned by Plasmic, and are regenerated on sync. They handle all the rendering of the design for you, and offer a highly flexible interface that lets you attach or override props on any elements in that component, or even replace or wrap elements altogether.

Plasmic also generates skeleton wrapper components—Feed, NewPost, Button, etc. You own these files—the file above is merely the starting boilerplate that Plasmic gives you. This is where you can add your own code—state/hooks, business logic, event handlers, etc. You can do what you want with this file—for instance, you can change it to be a class component, or you can choose not to even render the Plasmic presentational component and instead render something else.

By default, the default interface (DefaultFeedProps) that wrapper components start with take as props any slots and variants you defined on the component. Importantly, you can change the interface on these components, so that you control how it works.

Show real data

Now let’s install some dependencies:

yarn add uuid moment react-router-dom
yarn add -D @types/uuid @types/react-router-dom @types/react-router

Create the data model:

import {v4} from "uuid";
export interface PostEntry {
  id: string;
  createdAt: Date;
  content: string;
}

export function createPost(props: Omit<PostEntry, "id">): PostEntry {
  return {
    id: v4(),    ...props
  };
}

Make Feed take entries as props and wire that up to the presentational component.

import * as React from "react";
import { PlasmicFeed, DefaultFeedProps } from "./plasmic/minitwitter/PlasmicFeed";import { PostEntry } from "../model";import Post from "./Post";import moment from "moment";
interface FeedProps extends DefaultFeedProps {
  children?: never;
  entries: PostEntry[];}

function Feed({ entries, ...rest }: FeedProps) {  return (
    <PlasmicFeed
      {...rest}      postList={{        children: entries.map((entry) => (          <Post timestamp={moment(entry.createdAt).fromNow()}>            {entry.content}          </Post>        )),      }}    />
  );
}

export default Feed;

This shows how we can freely modify the interface to our component—you don’t have to stick with the default interface (DefaultFeedProps).

It also shows how the Plasmic presentational components work—they provide very flexible interface that let you override any prop (in this case children) on any element (in this case the postList) in that presentational component.

Create some dummy entries in the top-level App component:

import React, { useState } from 'react';import Feed from "./components/Feed";
import { createPost } from "./model";
function App() {
  const [entries, setEntries] = useState([    createPost({content: "Hello world", createdAt: new Date()}),    createPost({content: "Another post", createdAt: new Date()}),  ]);  return <Feed entries={entries}/>;}

export default App;

Now you should see your two dummy posts show up. (No need to edit the Post component.)

Add posts

Now let’s make it possible to add new posts by implementing the NewPosts component:

import * as React from "react";
import {
  PlasmicNewPost,  DefaultNewPostProps,
} from "./plasmic/minitwitter/PlasmicNewPost";
import { createPost, PostEntry } from "../model";import { useState } from "react";import { useHistory } from "react-router";
interface NewPostProps extends DefaultNewPostProps {
  children?: never;
  onAdd: (entry: PostEntry) => void;}

function NewPost({ onAdd, ...rest }: NewPostProps) {  const [content, setContent] = useState("");  const history = useHistory();  return (
    <PlasmicNewPost
      {...rest}      postContent={{        autoFocus: true,        value: content,        onChange: (e) => {          setContent(e.target.value);        },      }}      postButton={{        onClick: () => {          onAdd(            createPost({              content,              createdAt: new Date(),            })          );          history.push("/");        },      }}    />
  );
}

export default NewPost;

You should get a build error. The Button component (which the postButton element above is an instance of) doesn’t yet take an onClick handler, so let’s add that prop to Button:

import * as React from "react";
import {
  PlasmicButton,  DefaultButtonProps
} from "./plasmic/minitwitter/PlasmicButton";

interface ButtonProps extends DefaultButtonProps {
  children?: never;
  onClick?: () => void;}

function Button(props: ButtonProps) {
  return <PlasmicButton {...props} />;
}

export default Button;

Notice that we simply forwarded the onClick to the PlasmicButton. This is because Plasmic presentational components can take any props that the root element can take, and just forwards them onto the root element. For instance:

<PlasmicButton
  // All these props can be specified and are just forwarded to the root
  // element (in this case, a `button` element).
  onClick={...}
  title={...}
  style={...}
/>

Finally, add routing to App, since we now have two screens to navigate between:

import React, { useState } from "react";
import Feed from "./components/Feed";
import { createPost } from "./model";
import { Route, Switch } from "react-router";import { BrowserRouter } from "react-router-dom";import NewPost from "./components/NewPost";
function App() {
  const [entries, setEntries] = useState([
    createPost({ content: "Hello world", createdAt: new Date() }),    createPost({ content: "Another post", createdAt: new Date() }),  ]);
  return (
    <BrowserRouter>      <Switch>        <Route path={"/post"}>          <NewPost onAdd={(entry) => setEntries([entry, ...entries])} />        </Route>        <Route path={"/"}>          <Feed entries={entries} />
        </Route>      </Switch>    </BrowserRouter>  );
}

export default App;

Try it out!

Use localStorage

Notice that you try posting twice, the first post is gone. This is because the FAB is a normal link, and the whole page is reloaded (and all app state gone). One solution is to change the anchor to a react-router Link, which performs a fast non-page-loading transition that doesn’t lose any app state:

import * as React from "react";
import { PlasmicFab, DefaultFabProps } from "./plasmic/minitwitter/PlasmicFab";import { Link } from "react-router-dom";
interface FabProps extends DefaultFabProps {
  children?: never;
}

function Fab(props: FabProps) {
  return (
    <PlasmicFab
      {...props}
      root={{        render: (props) => {          const { ref, ...rest } = props;          // Remapping the props for Fab, which is an "a" in Plasmic,          // to a react-router-dom Link.          // We know ps.href is "/post", as it is set in Plasmic          return <Link {...rest} to={ps.href!}/>;        },      }}    />
  );
}

export default Fab;

Here we are referencing the root element of the component (you can always refer to an unnamed root element as just root). This render function is how Plasmic lets us replace the entire element or reconfigure how it’s rendered. In this case, we don’t want a normal a tag, we want a Link from react-router. The render function is passed the original set of props that were destined for the anchor tag, and we’re rewiring them to fit what Link expects.

But still, someone could open the link in a new tab, etc. We’ll instead make the state live in some simple browser localStorage. You can of course adapt this to work with any backend of your choosing, but localStorage is easy without getting caught up in the specifics of any particular storage backend.

import React, { useState } from "react";
import Feed from "./components/Feed";
import { createPost, PostEntry } from "./model";import { Route, Switch } from "react-router";
import { BrowserRouter } from "react-router-dom";
import NewPost from "./components/NewPost";

export function dumpPosts(entries: PostEntry[]) {  return JSON.stringify(entries);}
export function loadPosts(json: string): PostEntry[] {  const parsed: any[] = JSON.parse(json);  for (const item of parsed) {    item.createdAt = new Date(item.createdAt);  }  return parsed;}
function App() {
  const [entries, setEntries] = useState(() => {
    let loaded = localStorage["entries"];    if (loaded) {      return loadPosts(loaded);    }    return [
      createPost({ content: "Hello world", createdAt: new Date() }),
      createPost({ content: "Another post", createdAt: new Date() }),
    ];
  });
  return (
    <BrowserRouter>
      <Switch>
        <Route path={"/post"}>
          <NewPost
            onAdd={(entry) => {
              setEntries([entry, ...entries]);
              localStorage["entries"] = dumpPosts(entries);            }}
          />
        </Route>
        <Route path={"/"}>
          <Feed entries={entries} />
        </Route>
      </Switch>
    </BrowserRouter>
  );
}

export default App;

Building without limits

From here on out, you can keep building this app out. Some ideas:

  • Move to a “real” data store, such as Firebase, which is a great way to get started quickly and easily.
  • Add user auth so that posts include who authored them (Firebase Auth provides an easy way to implement this).
  • Add user pages, which show the feed of posts authored by that particular user.
  • Add user following system, so that you can follow or unfollow a user from their user page. Change the home feed for a user show only posts from those who the user follows.

Nothing here is specific to Plasmic—as you can see, the extent to which you can develop your app is unbounded, and Plasmic is here to streamline all the presentational aspects of building real products.

Further learning

The intermediate code tutorial covers building TodoMVC, which is a slightly more complex app with a few more moving parts—especially with respect to variants.

The main topic that this tutorial does not cover is how to work with variants from the code. For details, see the guide to the blackbox scheme.

You can also explore the slightly more advanced TodoMVC tutorial (which focuses just on the code), or explore the full Plasmic Developer Guide.