├── .gitignore ├── README.md ├── actions ├── PostActions.js ├── index.js └── types.js ├── components ├── AddPost.js ├── Head.js ├── Loading.js ├── Nav.js ├── PostItem.js └── PostList.js ├── lib └── db.js ├── next.config.js ├── package.json ├── pages └── index.js ├── reducers └── PostReducer.js ├── static └── favicon.ico └── store.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | .idea 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | /dist 13 | /.next 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | /package-lock.json 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | - [App Description](#app-description) 4 | - [Questions, Comments, Concerns](#questions-feedback) 5 | - [Dependencies and Inspiration](#dependencies-and-inspiration) 6 | - [Setting Up Cloud Firestore](#setting-up-cloud-firestore) 7 | - [Folder Structure](#folder-structure) 8 | - [Scripts](#scripts) 9 | - [npm run dev](#npm-run-dev) 10 | - [npm run build](#npm-run-build) 11 | - [npm run start](#npm-run-start) 12 | - [Setup and Running Example](#running-example) 13 | - [Live Demo](#live-demo) 14 | 15 | ## App Description 16 | 17 | This sample app demonstrates how to create a live list updated by user input in the provided status textbox using reactjs, next.js, redux, and cloud firestore (a firebase product). 18 | 19 | ## Questions, Comments, Concerns 20 | 21 | Please direct any questions, comments, or concerns to the [issues](https://github.com/NickDelfino/nextjs-with-redux-and-cloud-firestore/issues) section of the repo. Thanks for your feedback. 22 | 23 | ## Dependencies and Inspirations 24 | 25 | This project was seeded by [create-next-app](https://github.com/segmentio/create-next-app). For general questions about the setup of next.js and the structure visit the previously linked github. 26 | 27 | This project is inspired by the [examples](https://github.com/zeit/next.js/tree/master/examples) made by the nextjs team. 28 | It combines ideas from both their [with-redux](https://github.com/zeit/next.js/tree/master/examples/with-redux) 29 | and [with-firebase-authentication](https://github.com/zeit/next.js/tree/master/examples/with-firebase-authentication) examples 30 | to show how to create harmonization between redux and, in my case, cloud firestore. 31 | 32 | ## Folder Structure 33 | 34 | The project structure is made with going further in mind. Actions and reducers are moved 35 | into their own directories since there should be multiple of them in a full web app. A lib 36 | directory has been added for constant variable and to access the db. 37 | 38 | 39 | ``` 40 | my-app/ 41 | README.md 42 | package.json 43 | next.config.js 44 | components/ 45 | Head.js 46 | Nav.js 47 | AddPost.js 48 | Loading.js 49 | PostItem.js 50 | PostList.js 51 | pages/ 52 | index.js 53 | lib/ 54 | const.js 55 | db.js 56 | static/ 57 | favicon.ico 58 | reducers/ 59 | postReducer.js 60 | actions/ 61 | index.js 62 | types.js 63 | postActions.js 64 | store.js 65 | ``` 66 | 67 | ## Setting Up Cloud Firestore 68 | 69 | Cloud firestore setup is similar to firebase realtime database setup. 70 | 71 | The main thing that is need are the initialization keys generated when making a database 72 | on the site so the example app works. 73 | 74 | Go to [firebase.google.com](https://firebase.google.com/). Login or create an account if 75 | you don't already have one. 76 | 77 | Then, go to console. Select the "Add Project" square. 78 | 79 | Name your project anything you like and then on the following page select the option 80 | "Add Firebase to your web app". 81 | 82 | A model will appear with configuration information. Copy the entire config variable and 83 | paste it over the placeholder one in ../lib/db.js. 84 | 85 | Then you are good to go. Your app will start using your Cloud Firestore. 86 | 87 | ## Setup and Running Example 88 | 89 | To run the project simply clone this repository and navigate into it. 90 | 91 | Run npm install to acquire dependencies. 92 | 93 | Then simply perform the command `npm run dev`. 94 | 95 | ## Scripts 96 | 97 | ### `npm run dev` 98 | 99 | Runs the app in the development mode at [http://localhost:3000](http://localhost:3000). 100 | 101 | ### `npm run build` 102 | 103 | Builds the app for production to the `.next` folder.
104 | 105 | ### `npm run start` 106 | 107 | Starts the application in production mode. 108 | 109 | This script is made with heroku in mind. There is a port variable that needs to be 110 | specified for it to run. Heroku needs this for deployment. 111 | 112 | -------------------------------------------------------------------------------- /actions/PostActions.js: -------------------------------------------------------------------------------- 1 | import { loadDB } from '../lib/db'; 2 | import { 3 | UPDATE_USER_POST, 4 | FETCH_POSTS, 5 | ADD_POST_SUCCESS, 6 | ADD_POST_FAILURE 7 | } 8 | from './types'; 9 | 10 | //ACTIONS 11 | //This keeps the data in sync as the user types. 12 | //Also determines the length the user has left. 13 | export const updateUserPost = (userPost) => dispatch => { 14 | let newState = { 15 | userPost: userPost 16 | }; 17 | 18 | dispatch({ 19 | type: UPDATE_USER_POST, 20 | payload: newState 21 | }); 22 | }; 23 | 24 | //This adds the post to firebase cloudstore. 25 | //User typed data is reset upon success. 26 | //The failure action is fired but the reducer does not listen for it. 27 | //This is functionality that should be added in on a featured product. 28 | //For example, notify user the add failed and they should try again. 29 | export const addPost = (post) => async dispatch => { 30 | const db = await loadDB(); 31 | 32 | db.firestore().collection('posts') 33 | .add({ 34 | post: post, 35 | timestamp: db.firestore.FieldValue.serverTimestamp() 36 | }) 37 | .then(() => { 38 | dispatch({ 39 | type: ADD_POST_SUCCESS 40 | }); 41 | }) 42 | .catch((error) => { 43 | console.error("Error adding document: ", error); 44 | dispatch({ 45 | type: ADD_POST_FAILURE 46 | }); 47 | }); 48 | }; 49 | 50 | //This sets up the listener to fetch posts. 51 | //This pulls back an initial 50 posts but also sets 52 | //a listener so as new posts fill in their are added to the top. 53 | export const fetchPosts = () => async dispatch => { 54 | const db = await loadDB(); 55 | 56 | db.firestore().collection('posts') 57 | .orderBy('timestamp', 'desc') 58 | .limit(50) 59 | .onSnapshot(snapshot => { 60 | 61 | let newState = { 62 | posts: [] 63 | }; 64 | 65 | snapshot.forEach(function(doc) { 66 | newState.posts.push({ 67 | id: doc.id, 68 | post: doc.data().post 69 | }); 70 | }); 71 | 72 | dispatch({ 73 | type: FETCH_POSTS, 74 | payload: newState 75 | }) 76 | }); 77 | }; -------------------------------------------------------------------------------- /actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './PostActions'; 2 | -------------------------------------------------------------------------------- /actions/types.js: -------------------------------------------------------------------------------- 1 | //These are action types that can be broadcasted out and listened to. 2 | //Currently, ADDING_POST isn't being used by an actions or listened to. 3 | //ADD_POST_FAILURE is dispatched in the actions if adding a post fails but is not 4 | //being listened to in the reducer. 5 | export const FETCH_POSTS = 'FETCH_POSTS'; 6 | export const ADD_POST_SUCCESS = 'ADD_POST_SUCCESS'; 7 | export const ADD_POST_FAILURE = 'ADD_POST_FAILURE'; 8 | export const ADDING_POST = 'ADDING_POST'; 9 | export const UPDATE_USER_POST = 'UPDATE_USER_POST'; -------------------------------------------------------------------------------- /components/AddPost.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | import { Button } from 'react-bootstrap' 5 | import TextareaAutosize from 'react-autosize-textarea'; 6 | import { updateUserPost, addPost } from '../actions' 7 | 8 | class AddPost extends React.Component { 9 | //When user clicks add post fire the add post event. 10 | onClick(){ 11 | this.props.addPost(this.props.userPost); 12 | } 13 | 14 | //Update user text input as they type by firing this action. 15 | handleChange(event){ 16 | this.props.updateUserPost(event.target.value); 17 | } 18 | 19 | render() { 20 | const { userPost } = this.props; 21 | 22 | return( 23 |
24 | 36 |
37 | 43 |
44 | 62 |
63 | ); 64 | } 65 | } 66 | 67 | const mapDispatchToProps = (dispatch) => { 68 | return { 69 | updateUserPost: bindActionCreators(updateUserPost, dispatch), 70 | addPost: bindActionCreators(addPost, dispatch) 71 | } 72 | }; 73 | 74 | export default connect(state => state, mapDispatchToProps)(AddPost); -------------------------------------------------------------------------------- /components/Head.js: -------------------------------------------------------------------------------- 1 | import NextHead from 'next/head' 2 | 3 | const defaultDescription = ''; 4 | const defaultOGURL = ''; 5 | const defaultOGImage = ''; 6 | 7 | const Head = (props) => ( 8 | 9 | 10 | {props.title || ''} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | export default Head 28 | -------------------------------------------------------------------------------- /components/Loading.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by nickdelfino on 9/17/17. 3 | */ 4 | import React from 'react'; 5 | 6 | //Generic loading spinner. 7 | const LoadingSpinner = () => ( 8 |
9 | Loading... 10 |
11 | ); 12 | 13 | export default LoadingSpinner; -------------------------------------------------------------------------------- /components/Nav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import { Navbar } from 'react-bootstrap'; 4 | import Head from './Head'; 5 | 6 | //Basic nav component. 7 | const Nav = () => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | Post It 15 | 16 | 17 | 18 | 46 | 47 | ); 48 | 49 | export default Nav 50 | -------------------------------------------------------------------------------- /components/PostItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | //Displays the text from the posts that come in from the listener. 4 | const postItem = ({ post }) => ( 5 |
6 |

7 | {post.post} 8 |

9 | 29 |
30 | ); 31 | 32 | export default postItem; -------------------------------------------------------------------------------- /components/PostList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import PostItem from './PostItem'; 5 | import AddPost from '../components/AddPost'; 6 | import { fetchPosts } from '../actions'; 7 | import LoadingSpinner from './Loading'; 8 | 9 | //List component which loads posts as they come in. 10 | class postList extends React.Component{ 11 | static getInitialProps ({ store, isServer }) { 12 | store.dispatch(fetchPosts()); 13 | return { isServer } 14 | } 15 | 16 | //Determines if posts are loading. 17 | //This is determined by seeing if there are any posts yet. 18 | //This could also be done by dispatching an action 19 | //before setting up the listener to notify that loading 20 | //is happening on a prop. 21 | isLoading(){ 22 | const { posts } = this.props; 23 | if(posts && posts.length === 0){ 24 | return( 25 | 26 | ); 27 | } 28 | } 29 | 30 | //Start the listener when components mount. 31 | componentDidMount () { 32 | this.props.fetchPosts(); 33 | } 34 | 35 | //Renders the list and all the post items returned from the listener. 36 | render() { 37 | const { posts } = this.props; 38 | 39 | return ( 40 |
41 | 42 | {this.isLoading()} 43 | { 44 | posts.map((post, i) => ( 45 | 46 | )) 47 | } 48 | 59 |
60 | ) 61 | } 62 | } 63 | 64 | const mapDispatchToProps = (dispatch) => { 65 | return { 66 | fetchPosts: bindActionCreators(fetchPosts, dispatch) 67 | } 68 | }; 69 | 70 | export default connect(state => state, mapDispatchToProps)(postList) -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | import firebase from '@firebase/app'; 2 | import '@firebase/firestore' 3 | 4 | export function loadDB() { 5 | try { 6 | var config = { 7 | apiKey: "YOUR-DATA-HERE", 8 | authDomain: "YOUR-DATA-HERE", 9 | databaseURL: "YOUR-DATA-HERE", 10 | projectId: "YOUR-DATA-HERE", 11 | storageBucket: "YOUR-DATA-HERE", 12 | messagingSenderId: "YOUR-DATA-HERE" 13 | }; 14 | firebase.initializeApp(config); 15 | } catch (err) { 16 | // we skip the "already exists" message which is 17 | // not an actual error when we're hot-reloading 18 | if (!/already exists/.test(err.message)) { 19 | console.error('Firebase initialization error', err.stack); 20 | } 21 | } 22 | 23 | return firebase; 24 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: (config) => { 3 | // Fixes npm packages that depend on `fs` module 4 | config.node = { 5 | fs: 'empty' 6 | } 7 | 8 | return config 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-next-example-app", 3 | "scripts": { 4 | "dev": "next", 5 | "build": "next build", 6 | "start": "next start -p $PORT" 7 | }, 8 | "dependencies": { 9 | "@firebase/app": "^0.1.1", 10 | "@firebase/firestore": "^0.1.2", 11 | "firebase": "^4.6.0", 12 | "grpc": "^1.6.6", 13 | "next": "^4.1.3", 14 | "next-redux-wrapper": "^1.3.4", 15 | "react": "^16.0.0", 16 | "react-autosize-textarea": "^0.4.9", 17 | "react-bootstrap": "^0.31.5", 18 | "react-dom": "^16.0.0", 19 | "react-redux": "^5.0.6", 20 | "redux": "^3.7.2", 21 | "redux-devtools-extension": "^2.13.2", 22 | "redux-thunk": "^2.2.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { initStore } from '../store' 3 | import withRedux from 'next-redux-wrapper' 4 | import Nav from '../components/Nav'; 5 | import PostList from '../components/PostList'; 6 | 7 | class Posts extends React.Component { 8 | render () { 9 | return ( 10 |
11 |
14 | ) 15 | } 16 | } 17 | 18 | export default withRedux(initStore, null, null)(Posts) -------------------------------------------------------------------------------- /reducers/PostReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATE_USER_POST, 3 | FETCH_POSTS, 4 | ADD_POST_SUCCESS 5 | } 6 | from '../actions/types'; 7 | import { initialState } from '../store'; 8 | 9 | // REDUCERS 10 | export const PostReducer = (state, action) => { 11 | switch (action.type) { 12 | //On update user post just pass along the updated data. In this case, user inputted data is updated. 13 | case UPDATE_USER_POST: 14 | return Object.assign({}, state, action.payload); 15 | //On fetch posts just pass along the updated data. In this case, new posts. 16 | case FETCH_POSTS: 17 | return Object.assign({}, state, action.payload); 18 | //On add post success clear out the userpost. 19 | case ADD_POST_SUCCESS: 20 | return Object.assign({}, state, { userPost: ''}); 21 | default: 22 | return initialState 23 | } 24 | }; -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NickDelfino/nextjs-with-redux-and-cloud-firestore/ac5e2d3993b8f58ccba17f5a8ed43ca57c56fe39/static/favicon.ico -------------------------------------------------------------------------------- /store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { composeWithDevTools } from 'redux-devtools-extension' 3 | import thunkMiddleware from 'redux-thunk' 4 | import { PostReducer } from './reducers/PostReducer'; 5 | 6 | export const initialState = { 7 | posts: [], 8 | userPost: '' 9 | }; 10 | 11 | export const initStore = (initialState = initialState) => { 12 | return createStore(PostReducer, initialState, composeWithDevTools(applyMiddleware(thunkMiddleware))) 13 | }; --------------------------------------------------------------------------------