The Ello webapp is a
React application that
uses Redux for
unidirectional data flow and
react-router
for
client side routing.
We have mostly adhered to a pattern of Container
> Renderables
> Parts
.
An example is users:
UserContainer
>
UserRenderables
>
UserParts
The Container
is a
Component
connected to the redux store using
react-redux
's connect
method. This
gives us the ability to define a mapStateToProps
function that utilizes
selectors from reselect
to grab
immutable objects from the
redux store to map back on to the React component.
Using immutable objects is important here since it allows us to prevent React
from calling render
too often by hooking into the
shouldComponentUpdate
lifecycle method.
These are mostly high level like how a user is rendered in different cases such as compact, invitee, cards, and even the user detail page. (see UserRenderables)
We use
functional components
as much as possible since they are stateless and will just render if the
container allows it. In cases where we need a bit more control over the props or
lifecycle methods we would use a
PureComponent
.
These would be pieces that are reused in more than one Renderable
such as
username, links, location, etc. (see
UserParts
)
The
StreamContainer
is one of the key Containers
in the webapp. This container is able to take a
redux action that defines where
the data should be fetched and how it should be rendered for list, grid, error
or zero states like in
loadUserDrawer
.
This is where all of the data from api requests gets stored. Here is the basic format:
{
assets: {
1: { id: 1, hdpi: ..., ... },
2: { id: 2, hdpi: ..., ... }
},
pages: {
"/discover/recent": {
ids: ["3", "2", "1"],
pagination: {
next: "https://ello.co/api/v2/discover/posts/recent?before=something&per_page=25",
totalCount: NaN,
totalPages: NaN,
totalPagesRemaining: 0
},
type: "posts"
},
},
posts: {
1: { id: 1, token: "my-post-token-1", ... },
2: { id: 2, token: "my-post-token-2", ... }
},
profile: { ...currentUser },
users: {
1: { id: 1, username: "one", ... },
2: { id: 2, username: "two", ... }
}
}
The keys within the json
reducer are
mappingTypes
which match with the top level node of an API response so that they can be
parsed dynamically. The only exception to this is the pages
key which stores
the results of a page with information on how to get to the next page of
content.
Pages are typically tied directly to the pathname of the page i.e.:
/discover/recent
. But you can configure an action to have a resultKey
which
would override this default behavior for cases where more than one
StreamContainer
are on a page. Post detail pages are a good example of this
since they have comments/lovers/reposters. If you store each of those with their
own resultKey
you will be able to switch between them without the need to
refetch the data each time. The type
property of a page is the collection that
the ids
will be pulled from and the pagination
object contains how to get to
the next page and page counts.
A saga is a type of
redux middleware that
utilizes JavaScript
generators
to create side effects during the redux dispatch
flow using
redux-saga
.
Sagas allow the app to intercept actions that were dispatched and create other
side effects from them. An easy example would be when we logout of the app we
set a cookie and redirect the user back to /enter
as seen
here.
Sagas are very testable due to the nature of a generator and the fact that you
can step through them by calling the next
function and determining that the
return value is what was expected.
This is saga middleware that handles all API requests in the app and is the only
place that fetch
is ever called. It also handles generating other actions for
a request. If you dispatch a LOAD_STREAM
action it’s actually the requester
that creates the LOAD_STREAM_REQUEST
, LOAD_STREAM_SUCCESS
, and
LOAD_STREAM_FAILURE
actions based off of what happens during the fetch
.
These actions can now be added to a switch
of a reducer to modify state before
it ends up in a connected component’s mapStateToProps
function. It is also
responsible for obtaining the json and parsing the paging headers from a
request.
- Call
dispatch
from a connected component - Middleware can act on the action and can also
dispatch
side effects - Reducers respond to actions and update application state
mapStateToProps
is called on all connected components with updated state
When mapStateToProps
gets called the component will try to render
unless you
prevent it from doing so. Here are a couple ways to prevent this from happening:
- Implement
shouldComponentUpdate
in connected components. If this method returnsfalse
,render
won't get called again. - Make
Renderables
extendPureComponent
, which does a shallow compare to prevent re-renders.
- React Docs
- React Component Lifecycle
- Redux
- React Router v3 *good candidate to update to v4
react-redux
reselect
- Immutable Docs
redux-saga
To get the emojis.json file run:
curl -o public/static/emojis.json https://ello.co/emojis.json
To turn it on add this to your .env
:
USE_LOCAL_EMOJI=true
🤘💀🤘