An (alternate) starting point for Meteor and React.
This is a personal exercise and attempt to rewrite the tutorial on TheMeteorChef called Building a Blog With React to use React Router and Base v4.2.0, a system for jumpstarting a Meteor React project.
Please use this project in any way you want: as a learning tool for Meteor, React, or as a starting point for your own projects. Keep in mind that I wrote this as a learning experience, so take it with a grain of salt.
If you're using this as a learning tool, read the original tutorial first and recreate the tutorial in React Router yourself. This form of active learning can be daunting, but really helped me understand both Meteor and React. It'll do you good. I promise.
For amazing tutorials, please check out all the other free resources at TheMeteorChef.
The documentation is still a work in progress
The major differences from TheMeteorChef tutorial are using React Router instead of Flow Router, Komposer containers, replacing the use of Mixins with Imports, and using Base 4's newer way of organizing files.
Base 4 is significantly simpler and better organized, but the way some files are named can be confusing, so I've tried to improve the naming experience.
Please email me at hello@janzheng with suggestions, questions, comments, and bugs.
A project by Jan Zheng
I also added file and image handling to this base tutorial, since images and files popular with blogs. Although one could use something like the Imgur API, I wanted to experiment with a fully native experience through the use of file-collection and gridFS.
Even with Base 4's adherence to the new application structure per the Meteor Guide things get confusing very quickly. I've attempted to alleviate the confusion between file names through file naming conventions, yet still adhering to the Meteor Guide.
To make better sense of how a Meteor + React app is organized, I've attempted to map out the way files include each other. Check out the Organization section and the App Map & Outline.
I also started a way to organize SCSS styling to adhere to React component organization with a BEM-like syntax. Check out the Styling section.
This walkthrough assumes you've understood the original Blog tutorial on the meteor chef, and that you have a decent familiarity with Meteor projects.
This app is at the core very similar to the original tutorial app on TheMeteorChef. The purpose of the walkthrough is to document the overarching steps I took to create this app from Base, as well as the differences and additions to the original tutorial, but is not meant to be an exhaustive tutorial.
The code is also fairly extensively commented to provide clarity.
Note that I've renamed, reorganized, and refactored many of the files, components, containers, pages, and so on to be more descriptive than their counterparts in the tutorial, and might also be slightly different from Base as well.
Make sure you have all the required packages.
- Initialize a basic meteor project. e.g.
meteor create react-blog
- Replace the default meteor folders with Base
- Finish installing Base with:
npm install
- Add speakingurl:
meteor add ongoworks:speakingurl
- Add commonmark:
meteor add themeteorchef:commonmark
- Add momentjs:
meteor add momentjs:moment
Since we're building a private blog, we make sure that /signup
is unavailable and that we add our admins manually.
- Remove
/signup
fromroutes.js
and frompublic-navigation.js
- Manually add users to
/startup/server/fixtures.js
- Note that fixtures has been changed to account for user roles (pun!)
- change the Application title to HD Buff in
/client/main.html
and in the navapp-navigation.js
- This example uses
extends React.Component
instead ofReact.createClasss
. Read more
Whereas the original tutorial split routing into two folders public and authenticated routes, we have no need for a similar split in React Router. routes.js
takes care of both public and authenticated routes.
- The
requireAuth
method takes care of auth flow routing inroutes.js
- Note the default Base 4 behavior routes to
/login
first, then routes through to the intended location. This behavior differs from the tutorial where is a component that appears on a page template for unauthenticated users. I decided to keep the Base 4 behavior for simplicity. - added Meteor component to show Loading state if user is logging in
- Note the default Base 4 behavior routes to
- In Base 4, every route is public by default. We have to add
onEnter={ requireAuth }
to every private route. Don't assume that a route is safe just because it doesn't appear in Nav. Always make sure to protect private routes. - Remember that order matters in routing. Authenticated routes should come before public routes, and the 404 catchall route should be at the end.
/posts
- for posts for editing, and includes unpublished posts
<PostsList />
/posts/:_id/edit
- editing an individual post
<Editor />
– added authRedirect component to routes to redirect logged in users to /posts
/
- show all posts directly through Posts list, without a "homepage" or landing page
<PostsIndex />
–/login
- funny enough, if the
/tags/:tag
line is abovelogin
,/tags/login
will resolve to the login screen <Login />
/posts/:slug
- readable URLs using the post slug, e.g. blog.com/posts/this-is-a-post
<SinglePost />
/tags/:tag
- show all posts by a tag, again using the PostsIndex component from before
<PostsIndex />
/signup
- removed from Base since we're hard coding all accounts because this is a private blog
- This is the main container – all routes are passed through this all children as props
- All navigation is handled in the
AppNavigation
component- Note: proptypes are extracted to the outside of the component from Base 4, to be more similar to how proptypes are handled in other components
One of the differences between Base 4 and the tutorial is the use of React-Komposer
containers to replace getMeteorData()
. More information can be found here. Basically React-Komposer
does the same thing, but in a way that is more in line with React's principles. There's also a new official Create Container
method by the Meteor Development Group. Read more about it here.
Basically, the way containers work is that you import
a component through a container. The container subscribes to a Meteor data collection, makes sure the data returned is expected, and passes the data along as props into the component.
- The Documents section has been included as a reference to Base 4, and has been completely untouched
The naming convention used in the tutorial was very confusing for me (e.g. posts
referred to a list of authenticated posts as opposed to a list of all posts), so I've renamed many of the files, containers, pages, and components to be more descriptive. Unfortunately, this will make the tutorial harder to follow as filenames are now different.
- Created a new
imports/api/posts
folder to handle blog posts- the
/api/
folder holds the collection and schema, publications, as well as insert, update, and remove methods for handling posts.
- the
- Posts schema
- added stricter Posts allow / deny rules to
posts/collection.js
(the latest Base 4.3 has these added as well for Documents)
- added stricter Posts allow / deny rules to
- Displaying Posts
- Route to
PostEditlistPage
added toroutes.js
for listing posts for authenticated users (originally this was calledPosts
) Added a component for displaying a list of authenticated posts
PostList__Auth(originally
PostsList`)PostList__Auth
imported through komposer containercontainers/post-list__auth.js
- The tutorial had a line
linked={ true }
when renderingPostsList
that always seemed to resolve to true, so the line was removed containers/post-list__auth.js
follows the tutorial's suggestion to add additional data to the posts object returned from thePosts
collection- I'm not sure if I'm a fan of adding data like
uid
,href
andlabel
like in thegetMeteorData
in theposts-list.jsx
part of the tutorial. I think this information might be better suited to the display components than the data container component
- I'm not sure if I'm a fan of adding data like
- Route to
- Adding Posts
- All post adding logic in
post-editlist-page.js
for a new post, as opposed to a newadd-posts.js
file as there aren't many lines of code
- All post adding logic in
- Post Editor
- We get a specific
postID
from the route as a page parameter, then retrieve the appropriate post to edit. We then pass this post information intopost-edit.js
- Renamed editor subscription from
editor
topostById
post-edit.js
: Note that the accompanying functionality is combined into a large React component- Form validation uses
login.js
's' way of validating, and getting input values - Post Editor uses
{ PostUpdate }
to process and validate the form - PostEdit can't be stateless (since we're using
slug
state andcomponentDidMount()
, so we use ES6 syntax to extend React.Component - We define the display variables like date within render() instead of using a ton of helper functions, to be more readable
- I used a React Component because PostEdit needs to use state variables
- Added a
post-update.js
module to handle post edit updating - Added an
isChecked
function toget-input-value.js
for checkboxes - Added stricter validation schema for upsert in
posts/methods.js
- In order to be consistent with the tutorial, I've created
posts.js
which renders a list of posts inposts-list.js
, but this seems to unnecessarily add more files / makes it convoluted
- We get a specific
- Post Index
- When generating tags, I'm checking for "if (tags[0])" since the code will generate empty arrays if tags don't exist
- Post
- Added an if statement to prevent post body from displaying in post list
get-input-value.js
– added a helper to get checked values
All files are organized according to Base 4, so many of the files and folders are differently laid out than in the tutorial. I think it's for the better, as Base 4 has fewer files and folders, so finding what you need is less of a rabbit hunt.
Naming is very important, as it can easily help developers, especially someone from outside the project, understand how everything fits together. The tutorial makes some hasty decisions on naming files, collections, and react components that can add to confusion. It's important to remember that the ultimate goal is to keep things simple, understandable and easily maintainable, sometimes even at the cost of consistency.
For example, the component for displaying all public posts is called PostsIndex
and the publication is named postsIndex
, which makes it easy to understand that they're connected, but does not describe what the publication does.
This sections shows my thinking in establishing a better pattern for understandability and searchability.
The tutorial used some very confusing names for components, containers, and pages regarding Posts. It's not clear at all that for example that posts-list
is for authenticated users only. Names have been changed to indiciate better what the component does, and __
is used to denote a differentiating attribute (for example authenticated vs. public post list). Attributes are attached to the end of a name (e.g. PostList__Auth) to keep the component name listed together in the file browser.
We should also take care to ensure that the Component name matches that of a file name, as this will make importing components more intuitive.
posts-list.js
File ->post-list__auth.js
PostsList
Component ->PostList__Auth
public-post.js
File ->list-list__public.js
PublicPosts
Component ->PostList__Public
The API folder combines files for both the client and server. Although methods.js
and publications.js
are identically named, the Collection and Schema file (e.g. posts.js
) are different, breaking consistency. I've renamed them collection.js
to be consistent.
Note that the /api/documents/ folder has been left alone
api/files/files.js
->collection.js
api/posts/posts.js
->collection.js
Collections are capitalized to indicate a collection. These aren't really used very often, except for when debugging using the Meteor Mongo interface, so they're ok being short.
Posts
CollectionDocuments
CollectionFiles
Collection – note that the file-collection package uses the collection name as part of the URL by default, so the url would behttp://localhost:3000/gridfs/Files/[filename]
. However, it doesn't seem like file-collection is case-sensitive.
Meteor publications should be clear as to what information they yield. Publications should not be named for which components they'll exist in, but instead what kind of data they offer.
posts
->allPosts
postsIndex
->publishedPosts
tagsIndex
->publishedPostsByTag
singlePost
->singlePostBySlug
editor
->singlePostById
Modules are generally reusable methods and sometiems helper functions that reduce the amount of code squeezed into a component page. These files should either describe what the methods do, and not the component or file that imports them.
A component that needs to render something belongs in /ui/components/
.
Containers wrap reactive Meteor data subscriptions around React components. As such, each container have a corresponding React component. Containers use the same name as their Component counterparts, but have been prefixed with __
to clearly distinguish that these are containers, and that there is a component with the same name.
To avoid confusion between components and pages (especially in an editor like SublimeText), we attach Page
to the end of each Page file. This makes the routes list in routes.js
much easier to understand, as each route clearly points to a page.
Since many blogs have a file attachment or image requirement, I thought it would be fun to add file handling, and to push the boundaries of what I learned from the original TheMeteorChef tutorial.
I also added file and image handling to this base tutorial, since images and files popular with blogs. Although one could use something like the Imgur API, I wanted to experiment with a fully native experience through the use of file-collection and gridFS.
Goal: Add user-uploaded header images to each blog post, and a files list
SECTION UNFINISHED
- add file-collection
meteor add vsivsi:file-collection
- add jquery cookie
meteor add benjaminrh:jquery-cookie
- add underscore
meteor add underscore
– used for file-collection handling - create a new collection
- update
/startup/server/api.js
with a references tofiles
collection – '../../api/files/` ... - copy /posts/ into a new /files/ and rename references to files
- update
- new routes
- new /api/
- pages
The App Map doesn't show the file structure, but the import
structure of files – specifically, what files need other files to run, and exactly how the maze of files and folders works. I feel like a chart like this is necessary for much larger projects with hundreds of files, as the organization could get very confusing.
public/
,tests/
,server/
,clients/
are native meteor runtime foldersimport/
is dynamically imported files during runtimeapi/server/
is important to differentiate between client-side and server-side data functionality
- public/ # public images and assets
- tests/ # unit tests
∟ denotes an import
[Documents] – API shortcut to //imports/api/documents
[Posts] – API shortcut to //imports/api/posts
[Files] – API shortcut to //imports/api/files
server/ # Meteor server runtime
main.js # (imports only) main server-side runtime entry point
∟ //imports/startup/server/index.js # (imports only)
∟ accounts/email-templates.js # email templates (ex: forgot password email)
∟ fixtures.js # hardcoded users and server rules
∟ browser-policy.js # used for special circumstances, like with amazon
∟ api.js # (imports only) Collection publications and methods
∟ [Documents] server/publications.js # Meteor.publish('documents', ...)
∟ [Documents] documents.js # collection & schema
∟ [Documents] methods.js # methods (like inserting, deleting) for documents
∟ [Documents] documents.js
∟ [Posts] server/publications.js # Meteor.publish('posts', ...)
∟ [Posts] collection.js # collection & schema
∟ [Posts] methods.js
∟ [Posts] collection.js
∟ [Files] server/publications.js # Meteor.publish('files', ...)
∟ [Files] collection.js # collection & schema
∟ [Files] methods.js
∟ [Files] collection.js
∟ denotes an import
Layout – a template with elements that exist across many pages
Pages – a page that renders info, possibly with containers and components
Container - a komposer wrapper for componens that gives a React component access to Meteor data
Components – a React partial that renders info, possibly data
UI Shortcuts for readability:
[Documents] – API shortcut to //imports/api/documents
[Posts] – API shortcut to //imports/api/posts
[Files] – API shortcut to //imports/api/files
[layouts] – shortcut to //imports/ui/layouts
[pages] – shortcut to //imports/ui/pages
[containers] – shortcut to //imports/ui/containers
[components] – shortcut to //imports/ui/components
client/ # Meteor client runtime'
stylesheets/ # .scss for the entire site
main.html # main html with #react-root that routes.js renders
main.js # [imports only] main client-side runtime / entry point
∟ //imports/startup/client/index.js # { Bert } defines app-level imports like Bert and bootstrap
∟ routes.js # react router routes
∟ [layouts] app.js # { App } acts as the general layout for everything
∟ [containers] __app-navigation.js # { AppNavigation } data container
∟ [components] app-navigation.js # { AppNavigation } general navigation, auth or public
∟ [components] navigation__public.js # { Navigation__Public }
∟ [components] navigation__authenticated.js # { Navigation__Authenticated }
Authenticated Routes
∟ [pages] documents # { Documents } Base example.
∟ [components] add-document.js # Add new documents component
∟ [Documents] methods.js # { insertDocument }
∟ [containers] __documents-list.js # Data container for { DocumentsList }
∟ [Documents] documents.js # Documents collection (named 'collection.js' in others)
∟ [components] loading.js
∟ [components] documents-list.js # { DocumentsList } display list of documents
∟ [components] document.js # { Document } handles update and remove
∟ [Documents] methods.js # { updateDocument, removeDocument }
∟ [pages] post-editlist-page.js # { PostEditlistPage} show list of editable posts
∟ [Posts] methods.js # { newPost }
∟ [containers] __post-list__auth.js # Data container for { PostList__Auth }
∟ [Posts] collection.js # get Posts data
∟ [components] loading.js
∟ [components] post-list__auth.js # { PostList__Auth } list of authenticated posts
∟ [pages] post-edit-page.js # { PostEditPage } edit one post
∟ [containers] __post-edit.js # Data container for { PostList__Auth }
∟ [Files] collection # File data for image handling
∟ [Posts] collection.js # get Posts data
∟ [components] loading.js
∟ [components] post-edit.js # { PostList__Auth } single post edit details
∟ [pages] post-edit-page.js # { PostEditPage } edit one post
∟ [containers] __post-edit.js # Data container for { PostList__Auth }
∟ [Files] collection # File data for image handling
∟ [Posts] collection.js # get Posts data
∟ [components] loading.js
∟ [components] post-edit.js # { PostList__Auth } single post edit details
∟ [pages] file-list-page.js # { FileListPage } list of files (file management)
∟ [components] file-add.js # { FileAdd } uploads a file to File collection
∟ [Files] methods.js # (various FileCollection methods)
∟ [containers] __file-list.js
∟ [Files] collection.js # retrieves the list of files
∟ [components] loading.js
∟ [components] file-list.js # { FileList } main file list component
∟ [components] file.js # { File } each file; includes
∟ [Files] collection.js # get Files collection
∟ [modules] file-helpers.js # { isTypeImage, getReadableFileSizeString } helpers
∟ [Files] methods.js # { deleteFile } grant the ability to delete a file from list
∟ [containers] __post-name.js # container for getting posts
∟ [Posts] collection.js # { Posts } collection
∟ [components] post-name.js # { PostName } display name and link to image's containing post
Public Routes
∟ [pages] login-page.js # { LoginPage } public posts list
∟ [modules] login.js
∟ [modules] get-input-value.js # { getInputValue } get input values from form
∟ [pages] not-found-page.js # { NotFoundPage } page not found / 404 catch all
∟ [pages] recover-password-page.js # { RecoverPasswordPage } password lost
∟ [modules] recover-password.js # { handleRecoverPassword }
∟ [modules] get-input-value.js # { getInputValue } get input values from form
∟ [pages] reset-password-page.js # { ResetPasswordPage } public posts list
∟ [modules] reset-password.js # { handleResetPassword }
∟ [modules] get-input-value.js # { getInputValue } get input values from form
∟ [pages] signup-page.js # { SignupPage } public posts list
∟ [modules] reset-password.js # { handleSignup }
∟ [modules] get-input-value.js # { getInputValue } get input values from form
∟ [pages] post-list-page.js # { PostListPage } public posts list
∟ [containers] __post-list__public # data container for displaying public posts
∟ [Posts] collection.js # Post data
∟ [components] loading.js
∟ [components] post-list__public.js # { PostList__Public } displaying posts publicly
∟ [components] post__public.js # { Post__Public } render posts in a list
∟ [modules] file-helpers.js # { getFileURL, isTypeImage }
∟ [pages] post-single-page.js # { PostSinglePage } public single post from slug
∟ [containers] __post-single__public # data container for a single post
∟ [Files] collection.js # File data for images
∟ [Posts] collection.js # Post data
∟ [components] loading.js
∟ [components] post__public.js # { Post__Public } displaying posts publicly
∟ [components] post__public.js # { Post__Public } render posts individually
∟ [modules] file-helpers.js # { getFileURL, isTypeImage }
The SCSS should generally not care about how the app is organized, but you can run into conflicts, so we can encapsulate and compartmentalize by components and functionality.
Added some minor styling into the base application.scss
file
- .post styling for public post list
- taller textarea for edit page
- add a server / data / publication / methods "documentation"
- draw the current "import" file tree in ReadMe
- handle file upload disconnects (handle resume, or handle garbage collection on componentWillMount() )
- add copious amounts of inline comments explaining every line (for others and your future self)
- organized SCSS naming scheme that matches new file component structure
- a way to document component file relationships
- finish up the writeup into markdown
- post on MeteorChef
- make a note in the original thread comments
- make a note on vsivsi:file-collection github issue #77 on how I handled garbage collection
- see if I did it right / get tips from them (re: no resumable uploads, garbage collect user's resumables by uniqueId)
- add Bert to file upload and delete
- add upload cancel
- garbage collection of canceled files
- hide signup, since this is only for crew
- hide the link on nav, hide the oute
- rename files to make more sense
- add file and posts structure to the map
- improve naming convention (e.g. posts-list is for authenticated posts, but file name doesn't reflect this)
- see if it makes any sense to move some stuff around or rename files to make more sense (api/posts/... for example don't have a base include file to organize everything) – make more human readable
- make more readable, and upload to github
Base Version | v4.2.0 |
---|---|
Meteor Version | v1.3.2.4 |