First, clone the project locally and move into the folder. Open your terminal and run:
$ git clone git@github.com:vogelino/hidden-perspectives-app.git
$ cd hidden-perspectives-app
We recommend nvm for handeling node.js versions. You can install it using cURL:
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
or Wget:
wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
Read about how to use nvm here. The currently used node version is defined in the .nvmrc
file.
You have to install the project's dependencies using Yarn.
$ yarn install
For the app to be able to connect to the GraphQL Backend, you have to add some variables to your environment Setup. Copy and edit the following files depending on the environment you wish to run your app into:
$ cp .env.local.sample .env.local && $EDITOR .env.local # For both development and production environments
$ cp .env.test.local.sample .env.test.local && $EDITOR .env.test.local # For the test environment
And adapt the variables to match your setup
yarn dev
Runs the app in development mode.
Open http://localhost:3000 to view it in the browser.
The page will automatically reload if you make changes to the code.
You will see the build errors and lint warnings in the console.
yarn test
Runs the test watcher in an interactive mode.
By default, runs tests related to files changed since the last commit.
yarn build
Builds the app for production to the build
folder.
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.
Your app is ready to be deployed.
yarn start
Serves the build on http://localhost:5000.
We are using Now for deploying the app. If you would like to use Now as well you should duplicate now.sample.json
. Rename the file to now.json
and configure it for your personal needs.
This project was bootstrapped using Create React App (CRA). CRA allows us to concentrate on business logic instead of spending lots of time on configuring the project build. Under the hood, CRA uses Webpack to build and bundle the javascript code and any other type of imported files.
As indicated in the name “Create React App”, CRA provides the foundation for create React apps. React is a front end library for building user interfaces. It focuses on the View part in the Model-View-Controller paradigm.
We use React App Rewired to enhance the configuration not included by default by Create React App. We use it, for instance, to add compilation of Styled Components, which, as the name indicates, allows the creation of css-styled React component. Click here to learn more about how we use Styled Components.
We use Eslint to lint the javascript code, thus making sure that we avoid syntax errors and that the code style remains consistent throughout the project. Eslint can be configured to adapt to any team's taste. We use Airbnb's preset which is widely accepted in the community and suits our needs really well too.
The Eslint configuration can be changed int the .eslintrc
file. The file .eslintignore
can be used to ignore files that aren't owned by the project or shouldn't be linted.
yarn lint
Runs eslint in the src
folder and fixes all errors that can be fixed automatically.
yarn lint-latest
Runs eslint on all javascript files that changed since the last commit. This script is executed as a git hook before each commit and push to prevent users to share code that doesn't respect the code style guidelines.
We use unit testing to make sure our components' functionality is working as expected, thus avoiding the creation of new bugs. Testing also serves as a self documentation, as tests describe really well the purpose of the code.
Unit testing is done using Jest which is built in Create React App (See above). Jest can be configured within the jest.config.js
file in the project's root.
Tests are named after the file they test. For example, if we need to test the file dateUtil.js
, we would create a new file next to it called dateUtil.test.js
. Automatically, jest would it recognize and run it.
We use Enzyme to test React components. Enzyme makes it easier to assert, manipulate, and traverse React components.
yarn test
Runs the tests in interactive mode. Useful when developing. Runs per default onyl files changed since the last commit, but all tests can be runned at any time by typing a
in the interactive test mode.
yarn run test-latest
This command is the equivalent of yarn test
but doesn't run in interactive mode and only the tests related to the files changed since the last commit are tested.
Here is an overview of the main technologies/libraries used in this project.
Name | Usage | Cost | License |
---|---|---|---|
React | For building the user interface’s view and view-logic | Free | MIT |
Apollo Client | For connecting the user interface with the GraphQL API and managing internal state | Free | MIT |
Styled Components | For styling the App | Free | MIT |
Recompose | A React utility belt for function components and higher-order components | Free | MIT |
Name | Usage | Cost | License |
---|---|---|---|
Webpack (create-react-app) | For the app’s build process (i.e. minification) | Free | MIT |
Babel (create-react-app) | For compiling state of the art Es2017 javascript code to cross browser compatible Es2015 code. | Free | MIT |
Name | Usage | Cost | License |
---|---|---|---|
Jest (create-react-app) | For unit testing (including react components) | Free | MIT |
Eslint | For making sure the source code style is the same style across the project | Free | MIT |
Name | Usage | Cost | License |
---|---|---|---|
React Storybook | For documenting the React Components (style guide) | Free | MIT |
Name | Why | Cost | Terms |
---|---|---|---|
Zeit now | Because it has a low cost, is extremely easy, has automatic ssl certificates, different servers on different continents and more | $50/mo | Terms of service |
Name | Usage | Cost | Terms of service |
---|---|---|---|
Graphcool | For the database and the GraphQL API all on one place | Pay as you go | Terms of service |
For the backend, we use the Graphcool Framework Cloud service, which takes care of hosting the data and provides us with a GraphQL API. This allows us to concentrate on the business logic on the frontend instead of dealing with complex server configurations.
To connect our app to the GraphQL endpoint and to manage internal state, we use Apollo GraphQL, more precisely the ApolloClient library. The Apollo Client is configured using different links, each offering a different way to query data:
This is useful to perform CRUD operations on the backend using the GraphQL API.
This is useful to subscribe to data changes on the backend using the backend GraphQL API.
This enables user authetication. Users can be authenticated and an auth-token retrieved from the backend GraphQL API is used to sign further rescricted operations.
This allows us to query local state the same way it is done with the backend GraphQL API. Using this technique enables us to use only one way of querying data instead of having an additional state management like Redux.
We use React Apollo to provide our React components with the ability to query the data they need to be rendered. Components then perform CRUD operation either from the backend GraphQL API or from the internal state using the GraphQL query language. These retrive data that can be used as component props.
The ApolloProvider is an Higher Order Component providing all components within the apps an instance to the ApolloClient using React's context. This allows components deeper in the tree to use the client to query the state and backend's data.
The ApolloProvider is given the ApolloClient instance and wraps the whole app once. This is done in the app's entry file index.js
:
ReactDOM.render(
(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
),
document.getElementById('root'),
);
To perform CRUD operations with the data or state, components can be wrap with Higher Order Components provided by React Apollo.
import gql from 'graphql-tag';
import { Query } from 'react-apollo';
const GET_DOGS = gql`
{
dogs {
id
breed
}
}
`;
const Dogs = ({ onDogSelected }) => (
<Query query={GET_DOGS}>
{({ loading, error, data }) => {
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
return (
<select name="dog" onChange={onDogSelected}>
{data.dogs.map(dog => (
<option key={dog.id} value={dog.breed}>
{dog.breed}
</option>
))}
</select>
);
}}
</Query>
);
Here a component is wrapped with the Query
Higher Order Component which accepts a GraphQL query and takes care of handling the error messages, the loading state and the data fetching.
Authentication is configured on the backend side using the Graphcool console. There, permission queries can be configured to restrict which operations can be performed by which users. Operations with the right auth-token in the headers are then authorised if they pass the permission query.
The frontend uses the authentication queries provided by the backend to gather an authentication token, which is then used to sign the operations made on the frontend. The auth-token is saved in the local storage, until it is invalidated after the expiration date is reached.
For the routing React Router DOM is used. Routes are defined in the App component. The router renders different React components based on the current url, therfore, components representing pages are place in a specific page folder called pages
.
To link between pages, React Router provides us with a Link component that accepts an to
property indicating the absolute path to browse to.
Components can also access the router's instance, because the router wraps the whole app and makes the router available via the context. To make use of the router within a component, use the withRouter
Higher Order Component provided by React Router to wrap your component. The router prop will then be passed as a component.
Assets like images, favicons, manifests, css files or other resources that need to be served on the server can be put in the public
folder.
This functionality is handled by Create React App and although it can be used to link to various resources like fonts or images, Create React App also provides most of the relevant webpack loaders to import resources directly into the javascript code. For instance importing a fonts.css
file from withing the entry file index.js
will import the css automatically and will additionally import the fonts linked within the css files. Knowing that, only put the assets that have standardized naming conventions and need to be served on the root (eg. favicons, manifest.json, browserconfig.xml, etc.).
To make sure that css defaults are the same on all browsers, we use the popular normalize.css
.
Global CSS rules can be defined in the index.css
laying in the root of the project's src
folder.
The meta tags are defined using [React Helmet](). React Helmet has a main file which allows us to defines the global meta tags used for the map. Additionally to the global tags, tags can be defined deeper in the components' hierarchy, thus overriding the options higher in the hierarchy.
To style the app, we use Styled Components. Styled Components provides an utility function that creates any html tag with a unique and scoped css class. This is a big advantage as it removes the need to use a naming convention like Block Element Identifier as the style defined in Styled Components cannot have any effect on sibling components.
Styled Components are created in a styles.js
file laying next to the component itself. Each component is exported as a named export. The utility function provided by Styled Components uses the power of tagged templates to make the styling using real css a breaze.
export const Container = styled.aside`
width: 20%;
max-width: 320px;
float: left;
`;
Styled Components can also be combined (inherited):
export const PrimaryButton = styled(NormalButton)`
background: pink;
color: white;
`;
Attributes can be added to components directly from Styled Components:
export const NormalButton = styled.button.attrs({
type: 'button'
})`
cursor pointer;
`;
Styled components can access the props to adapt their styling:
export const Button = styled.button`
cursor: ${({ disabled }) =>
(disabled ? 'default' : 'pointer')};
`;
Styled components are then imported from within the main component and used instead of the regular HTML/JSX Markup:
import {
Container,
Title,
Subtitle,
Content,
} from './styles';
const MyComponent = ({
title,
subtitle,
content,
}) => (
<Container>
<Title>{title}</Title>
<Subtitle>{subtitle}</Subtitle>
<Content>{content}</Content>
<Container>
);
React Storybook is used for documenting our React UI components library. Storybook runs as an indepedent website, in which each UI component is demonstrated interactively. Developers can use it to develop components, while being isolated from the rest of the App. Concurrently, designers can see and review the components, making sure they are in sync with their own design component library.
yarn storybook
This script runs the storybook in development mode, thus enabling hot module replacement.
yarn build-storybook
This scripts runs the storybook in production mode, prior to a development.
The configuration of the storybook lives inside the .storybook
folder. There, two files are present:
config.js
This is the configuration file that takes care to load all stories.
addons.js
This files contains configuration for the so called addons. Addons can add functionality to a storybook. For instance, we use the addon info
, which is helpful to display additional information with each component.
Stories are sets of pages within your component library that document one or more components. These are described within javascript files located in the src/stories
folder of the app.
// src/stories/index.js
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';
import { withInfo } from '@storybook/addon-info';
import { Button, Welcome } from '@storybook/react/demo';
storiesOf('Welcome', module).add('to Storybook', () => <Welcome showApp={linkTo('Button')} />);
storiesOf('Button', module)
.add('with text', withInfo(`
description or documentation about my component, supports markdown
~~~js
<Button>Click Here</Button>
~~~
`)(() => <Button onClick={action('clicked')}>Hello Button</Button>))
.add('with some emoji', () => <Button onClick={action('clicked')}>Yippieee!</Button>);
// ...