React: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
No edit summary
 
(43 intermediate revisions by the same user not shown)
Line 1: Line 1:
Quick Reference
Quick Reference
= Development Setup =
A good resource for this can be found https://jscomplete.com/learn/1rd-reactful' by Samer Buna<br />
A template for a project can be found https://github.com/jscomplete/rgs-template
= Production Setup =
Below are examples of dev vs production scripts
=== Packages build scripts ===
Additional changes to NPM build scripts for production
<syntaxhighlight lang="js">
    "test:ci": "jest",
    "clean:build": "rimraf ./build && mkdir build",
    "prebuild": "run-p clean:build test:ci",
    "build": "webpack --config webpack.config.prod.js",
    "postbuild": "run-p start:api serve:build",
    "serve:build": "http-server ./build"
</syntaxhighlight>
=== Webpack differences ===
Production
<syntaxhighlight lang="js">
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const webpackBundleAnalyzer = require("webpack-bundle-analyzer");
process.env.NODE_ENV = "production";
module.exports = {
  mode: "production",
  target: "web",
  devtool: "source-map",
  entry: "./src/index",
  output: {
    path: path.resolve(__dirname, "build"),
    publicPath: "/",
    filename: "bundle.js"
  },
  plugins: [
    // Display bundle stats
    new webpackBundleAnalyzer.BundleAnalyzerPlugin({ analyzerMode: "static" }),
    new MiniCssExtractPlugin({
      filename: "[name].[contenthash].css"
    }),
    new webpack.DefinePlugin({
      // This global makes sure React is built in prod mode.
      "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
      "process.env.API_URL": JSON.stringify("http://localhost:3001")
    }),
    new HtmlWebpackPlugin({
      template: "src/index.html",
      favicon: "src/favicon.ico",
      minify: {
        // see https://github.com/kangax/html-minifier#options-quick-reference
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true
      }
    })
  ],
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ["babel-loader", "eslint-loader"]
      },
      {
        test: /(\.css)$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              sourceMap: true
            }
          },
          {
            loader: "postcss-loader",
            options: {
              plugins: () => [require("cssnano")],
              sourceMap: true
            }
          }
        ]
      }
    ]
  }
};
</syntaxhighlight>
Development
<syntaxhighlight lang="javascript">
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
process.env.NODE_ENV = "development";
module.exports = {
  mode: "development",
  target: "web",
  devtool: "cheap-module-source-map",
  entry: "./src/index",
  output: {
    path: path.resolve(__dirname, "build"),
    publicPath: "/",
    filename: "bundle.js"
  },
  devServer: {
    stats: "minimal",
    overlay: true,
    historyApiFallback: true,
    disableHostCheck: true,
    headers: { "Access-Control-Allow-Origin": "*" },
    https: false
  },
  plugins: [
    new webpack.DefinePlugin({
      "process.env.API_URL": JSON.stringify("http://localhost:3001")
    }),
    new HtmlWebpackPlugin({
      template: "src/index.html",
      favicon: "src/favicon.ico"
    })
  ],
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ["babel-loader", "eslint-loader"]
      },
      {
        test: /(\.css)$/,
        use: ["style-loader", "css-loader"]
      }
    ]
  }
};
</syntaxhighlight>
===Source switches===
<syntaxhighlight lang="js">
// Use CommonJS require below so we can dynamically import during build-time.
if (process.env.NODE_ENV === "production") {
  module.exports = require("./configureStore.prod");
} else {
  module.exports = require("./configureStore.dev");
}
</syntaxhighlight>


=Variables=
=Variables=
<source lang="babel" line="line">
<syntaxhighlight lang="js" line="line">
  const myVar = (
  const myVar = (
   <h1>Love it</h1>
   <h1>Love it</h1>
  );
  );
</source>
</syntaxhighlight>
 
= Common Errors =
When using classes and you have forgotten to bind you get
Cannot read property 'setState' of undefined
 
This is because you have not bound the function in the class to this e.g.
<syntaxhighlight lang="js">
this.onTitleChange = this.onTitleChange.bind(this);
</syntaxhighlight>
 
= Form Example =
== Classes ==
Below is the old class approach with binding and constructor
<syntaxhighlight lang="jsx">
 
class InputPage extends React.Component {
  constructor(props) {
    super(props);
 
    this.state = {
      course : {
        title: ""
      }
    };
 
    this.onTitleChange = this.onTitleChange.bind(this);
  }
 
  onTitleChange(event) {
    this.setState({
      course: {
        title: event.target.value,
      },
    });
  }
 
  render() {
    return (
      <form >
        <input
          type="text"
          value={this.state.course.title}
          onChange={this.onTitleChange}
        />
      </form>
    );
  }
}
 
</syntaxhighlight>
 
And now old class approach without binding and constructor and using spread
 
<syntaxhighlight lang="jsx">
 
class InputPage extends React.Component {
 
    this.state = {
      course : {
          title: ""
      }
    }
 
    onTitleChange = event => {
        const course = {...this.state.course, title: event.target.value}
        this.setState( {course} )
    }
 
    render() {
        return (
        <form >
            <input
            type="text"
            value={this.state.course.title}
            onChange={this.onTitleChange}
            />
        </form>
    )
}
 
</syntaxhighlight>
 
=Hooks=
 
Hooks can only be used at the top level. You can set rules in your editor to ensure this.
 
== UseState ==
Example of setting state
 
<syntaxhighlight lang="jsx">
const InputElement = () => {
 
    const [inputText, setInputText] = useState("")
   
    return (
        <input placeholder="some text" onChange={(event) => { setInputText(event.target.value) } } />
    )
}
 
</syntaxhighlight>
 
== UseRef ==
You can get a reference from the DOM using UseRef
 
<syntaxhighlight lang="jsx">
    const imageRef = useRef(null)
 
    console.info("Got here")
 
    return (
        <img
            onMouseOver={ ()=>{ imageRef.current.src = secondaryImg} }
            onMouseOut={ ()=>{ imageRef.current.src = primaryImg} }
            src={primaryImg}
            ref={imageRef}
        />
        )
</syntaxhighlight>
 
== UseEffect ==
The first argument to useEffect is a function which executes when the component first mounts. The first argument of the return should be a function which remove the resources, if applicable, used as the first argument. e.g.
<br />
Add and remove scroll handler
 
<syntaxhighlight lang="jsx">
    useEffect( () = > {
      window.addEventListener("scroll", scrollHandler)
      return ( ()=> {
          window.removeEventListener("scroll", scrollHandler)
      })
    })
</syntaxhighlight>
 
The second argument is a array of values which, if changed, will cause the effect to be called again.
 
e.g. Maybe a checkbox value
 
<syntaxhighlight lang="jsx">
    const [checkboxValue, setCheckboxValue] = useState(false)
 
    useEffect( () = > {
      window.addEventListener("scroll", scrollHandler)
      return ( ()=> {
          window.removeEventListener("scroll", scrollHandler)
      }, [checkboxValue])
    })
</syntaxhighlight>


=Run=
The effect will be called each time the checkbox is clicked.
 
== UseConfig ==
This allows passing of config to different pages.
<br />
 
Top Level, wraps a context around pages
 
<syntaxhighlight lang="jsx">
    import React from "react";
    import Home from "./Home";
    import Speakers from "./Speakers";
 
    export const AppContext = React.createContext();
 
    const pageToShow = pageName => {
      if (pageName === "Home") return <Home />;
      if (pageName === "Speakers") return <Speakers />;
      return <div>Not Found</div>;
    };
 
    const configValue = {
      showSpeakerSpeakingDays : true
    }
 
    const App = ({ pageName }) => {
      return  <AppContext.Provider value={configValue}>
                <div>{pageToShow(pageName)}</div>
              </AppContext.Provider>
    };
 
    export default App;
 
</syntaxhighlight>
 
A the usage on the page.
 
<syntaxhighlight lang="jsx">
    import React, { useState, useEffect, useContext} from "react";
    import  { AppContext } from "./App"
 
    const PageFunction = ({}) => {
      const context = useContext(AppContext)
    }
 
</syntaxhighlight>
 
== UseReducer ==
A reducer is defined as something which takes a previous state and function and returns a new state
(previousState, action) => newState
<br />
 
UseState is built on Reducer i.e.
 
<syntaxhighlight lang="jsx">
    const [speakerList, setSpeakerList] = useState([])
</syntaxhighlight>
 
Is the same as
 
<syntaxhighlight lang="jsx">
    const [speakerList, setSpeakerList] = useReducer( (state,action) => action, [])
</syntaxhighlight>
 
Is the same as
 
<syntaxhighlight lang="jsx">
    function speakerReducer(state, action) {
        return action
    }
 
    const [speakerList, setSpeakerList] = useReducer( speakerReducer, [])
</syntaxhighlight>
 
We can now make this work the same a the useState
 
<syntaxhighlight lang="jsx">
    function speakerReducer(state, action) {
        switch(action.type) {
            case "setSpeakerList" : {
                return action.data;
            }
            default:
                return state;
        }
    }
 
    // So replaced setSpeakList, which is not returned with a dispatch name, we called it dispatch but we could have called derek
 
    // const [speakerList, setSpeakerList] = useReducer( speakerReducer, [])
    const [speakerList, dispatch] = useReducer( speakerReducer, [])
</syntaxhighlight>
 
So the old usage was
<syntaxhighlight lang="jsx">
    setSpeakerList(mySpeakerList)
</syntaxhighlight>
 
Using the reducer gives
<syntaxhighlight lang="jsx">
    dispatch( {
        type: "setSpeakerList",
        data: mySpeakerList
    })
</syntaxhighlight>
 
== UseCallback ==
This caches a function. Given the function below,
 
<syntaxhighlight lang="jsx">
  const heartFavoriteHandler = (e, favoriteValue) => {
    e.preventDefault();
    const sessionId = parseInt(e.target.attributes["data-sessionid"].value);
   
    dispatch({
      type : favoriteValue === true ? "setFavourite" : "setUnfavourite",
      sessionId
    });
</syntaxhighlight>
 
We can cache this using UseCallback
 
<syntaxhighlight lang="jsx">
  const heartFavoriteHandler = useCallback( (e, favoriteValue) => {
    e.preventDefault();
    const sessionId = parseInt(e.target.attributes["data-sessionid"].value);
   
    dispatch({
      type : favoriteValue === true ? "setFavourite" : "setUnfavourite",
      sessionId
    }, [] );
</syntaxhighlight>
 
However for this to work you will need to wrap the content it is reloading in React.memo. In this case it was SpeakerDetail
 
<syntaxhighlight lang="jsx">
          <div className="card-deck">
            {speakerListFiltered.map(
              ({ id, firstName, lastName, bio, favorite }) => {
                return (
                  <SpeakerDetail
                    key={id}
                    id={id}
                    favorite={favorite}
                    onHeartFavoriteHandler={heartFavoriteHandler}
                    firstName={firstName}
                    lastName={lastName}
                    bio={bio}
                  />
                );
              }
            )}
          </div>
</syntaxhighlight>
 
so this gave
 
<syntaxhighlight lang="jsx">
 
const SpeakerDetail = React.memo( ({
                          id,
                          firstName,
                          lastName,
                          favorite,
                          bio,
                          onHeartFavoriteHandler
                      }) => {
    console.log(`SpeakerDetail:${id} ${firstName} ${lastName} ${favorite}`);
    return (
        <div className="card col-4 cardmin">
            <ImageToggleOnScroll
                className="card-img-top"
                primaryImg={`/static/speakers/bw/Speaker-${id}.jpg`}
                secondaryImg={`/static/speakers/Speaker-${id}.jpg`}
                alt="{firstName} {lastName}"
            />
            <div className="card-body">
                <h4 className="card-title">
                    <button
                        data-sessionid={id}
                        const mySortedSpeakList = useMemo( ()=> speakerList
    .filter(
      ({ sat, sun }) => (speakingSaturday && sat) || (speakingSunday && sun)
    )
    .sort(function(a, b) {
      if (a.firstName < b.firstName) {
        return -1;
      }
      if (a.firstName > b.firstName) {
        return 1;
      }
      return 0;
    }), [speakingSaturday,speakingSunday,speakerList]); className={favorite ? "heartredbutton" : "heartdarkbutton"}
                        onClick={e => {
                            onHeartFavoriteHandler(e, !favorite);
                        }}
                    />
                    <span>
            {firstName} {lastName}
          </span>
                </h4>
 
                <span>{bio}</span>
            </div>
        </div>
    );
});
 
</syntaxhighlight>
 
== UseMemo ==
Similarly you can cache a value using UseMemo. e.g.
 
<syntaxhighlight lang="jsx">
  const mySortedSpeakerList = useMemo( ()=> speakerList
    .filter(
      ({ sat, sun }) => (speakingSaturday && sat) || (speakingSunday && sun)
    )
    .sort(function(a, b) {
      if (a.firstName < b.firstName) {
        return -1;
      }
      if (a.firstName > b.firstName) {
        return 1;
      }
      return 0;
    }), [speakingSaturday,speakingSunday,speakerList]);
</syntaxhighlight>
 
The value will only change if one of the speakingSaturday,speakingSunday,speakerList changes
 
== Custom Hook ==
An example of a reducer with a custom hook using axios
 
<syntaxhighlight lang="jsx">
const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case "FETCH_INIT":
      return { ...state, isLoading: true, isError: false };
    case "FETCH_SUCCESS":
      return {
        ...state,
        isLoading: false,
        hasErrored: false,
        errorMessage: "",
        data: action.payload
      };
    case "FETCH_FAILURE":
      return {
        ...state,
        isLoading: false,
        hasErrored: true,
        errorMessage: "Data Retrieve Failure"
      };
    case "REPLACE_DATA":
      // The record passed (state.data) must have the attribute "id"
      const newData = state.data.map(rec => {
        return rec.id === action.replacerecord.id ? action.replacerecord : rec;
      });
      return {
        ...state,
        isLoading: false,
        hasErrored: false,
        errorMessage: "",
        data: newData
      };
    default:
      throw new Error();
  }
};
</syntaxhighlight>
 
<syntaxhighlight lang="jsx">
const useAxiosFetch = (initialUrl, initialData) => {
  const [url] = useState(initialUrl);
 
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    hasErrored: false,
    errorMessage: "",
    data: initialData
  });
 
  useEffect(() => {
    let didCancel = false;
 
    const fetchData = async () => {
      dispatch({ type: "FETCH_INIT" });
 
      try {
        let result = await axios.get(url);
        if (!didCancel) {
          dispatch({ type: "FETCH_SUCCESS", payload: result.data });
        }
      } catch (err) {
        if (!didCancel) {
          dispatch({ type: "FETCH_FAILURE" });
        }
      }
    };
 
    fetchData();
 
    return () => {
      didCancel = true;
    };
  }, [url]);
 
  const updateDataRecord = record => {
    dispatch({
      type: "REPLACE_DATA",
      replacerecord: record
    });
  };
 
  return { ...state, updateDataRecord };
};
 
</syntaxhighlight>


=Maps=
=Maps=
<source lang="babel" line="line">
<syntaxhighlight lang="jsx" line="line">


  // Only do this if items have no stable IDs
  // Only do this if items have no stable IDs
Line 20: Line 648:
   {todo.text}
   {todo.text}
  </li>
  </li>
);</source>
);</syntaxhighlight>


=Component Example=
=Component Example=


<source lang="babel" line="line">
<syntaxhighlight lang="javascript" line="line">
import React, {Component} from 'react';
import React, {Component} from 'react';
import ReactDOM from 'react-dom';  
import ReactDOM from 'react-dom';  
Line 37: Line 665:
   }   
   }   
};
};
</source>
</syntaxhighlight>


=Passing Props to child=
=Passing Props to child=
Line 43: Line 671:
Parent.json
Parent.json


<source>
<syntaxhighlight lang="javascript">
import React from 'react';
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOM from 'react-dom';
Line 61: Line 689:
ReactDOM.render(<Parent />, document.getElementById('app'));
ReactDOM.render(<Parent />, document.getElementById('app'));


</source>
</syntaxhighlight>


Child.json
Child.json


<source>
<syntaxhighlight lang="javascript">
import React from 'react';
import React from 'react';


Line 73: Line 701:
   }
   }
}
}
</source>
</syntaxhighlight>
 
=Forms with Ref=
You can capture import using the React.createRef() e.g.
 
<syntaxhighlight = lang="javascript">
class Form extends React.Component
{
  userNameInput = React.createRef();
 
  handleSubmit = (event) => {
    event.preventDefault()
    console.log(userNameInput.current.value)
  }
  render()
  {
      return (
          <form onSubmit={this.handleSubmit} >
              <input
                  type="text"
                  placeHolder="background text"
                  ref={this.userNameInput}
                  required
              />
          />
      )
   
  }
}
</syntaxhighlight>
 
=Forms without Ref=
A Better way may be to use a state to hold the name.
 
<syntaxhighlight = lang="javascript">
class Form extends React.Component
{
  state = { userName : ''}
 
  render()
  {
      return (
          <form onSubmit={this.handleSubmit} >
              <input
                  type="text"
                  value={this.state.userName}
                  OnChange = {
                      event => {
                          this.setState( {userName: event.target.value} )
                      }
                  }
                  required
              />
          />
      )
   
  }
}
</syntaxhighlight>


=Binding a function=
=Binding a function=
<source>
<syntaxhighlight = lang="javascript">


import React from 'react';
import React from 'react';
Line 158: Line 844:
   }
   }
}
}
</source>
</syntaxhighlight>


=Inline Styling=
=Inline Styling=
React needs double quotes to inject style in JSX e.g.
React needs double quotes to inject style in JSX e.g.
<source>
<syntaxhighlight lang="javascript">
 
import React from 'react';
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOM from 'react-dom';
Line 172: Line 859:
document.getElementById('app')
document.getElementById('app')
);
);
</source>
</syntaxhighlight>
Another example
Another example
<source>
<syntaxhighlight lang="javascript">
mport React from 'react';
mport React from 'react';
import ReactDOM from 'react-dom';
import ReactDOM from 'react-dom';
Line 190: Line 877:
document.getElementById('app')
document.getElementById('app')
);
);
</source>
</syntaxhighlight>
You can write px styles without quotes e.g.
You can write px styles without quotes e.g.
<source>
<syntaxhighlight lang="javascript">
const styles = {
const styles = {
   background: 'lightblue',
   background: 'lightblue',
Line 199: Line 886:
   fontSize: 50
   fontSize: 50
}
}
</source>
</syntaxhighlight>
 


=Stateless Classes=
=Stateless Classes=
A component class written in the usual way:
A component class written in the usual way:


<source>
<syntaxhighlight lang="javascript">
export class MyComponentClass extends React.Component {
export class MyComponentClass extends React.Component {
   render() {
   render() {
Line 216: Line 902:
   return <h1>Hello world</h1>;
   return <h1>Hello world</h1>;
}
}
</source>
</syntaxhighlight>


You can pass the properties from the container class like this
You can pass the properties from the container class like this


<source>
<syntaxhighlight lang="javascript">


import React from 'react';
import React from 'react';
Line 233: Line 919:
   );
   );
}
}
</source>
</syntaxhighlight>


=Prototypes=
=Prototypes=
Line 239: Line 925:
Add them at the bottom so people can see how to use the code
Add them at the bottom so people can see how to use the code


<source>
<syntaxhighlight lang="javascript">


import React from 'react';
import React from 'react';
Line 270: Line 956:
   weeksOnList: React.PropTypes.number.isRequired,
   weeksOnList: React.PropTypes.number.isRequired,
};
};
</source>
</syntaxhighlight>


And a render example
And a render example


<source>
<syntaxhighlight lang="javascript">
import React from 'react';
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOM from 'react-dom';
Line 306: Line 992:


ReactDOM.render(<BookList />, document.getElementById('app'));
ReactDOM.render(<BookList />, document.getElementById('app'));
</source>
</syntaxhighlight>


And for stateless functions
And for stateless functions


<source>
<syntaxhighlight lang="javascript">
import React from 'react';
import React from 'react';


Line 328: Line 1,014:
   src: React.PropTypes.string.isRequired
   src: React.PropTypes.string.isRequired
};
};
</source>
</syntaxhighlight>


=Form Example where we track the input value=
=Form Example where we track the input value=
<source>
<syntaxhighlight lang="javascript">


import React from 'react';
import React from 'react';
Line 363: Line 1,049:
document.getElementById('app')
document.getElementById('app')
);
);
</source>
</syntaxhighlight>
 
= The New Way Example using custom hooks =
This is the example from the react course where they broke the game into a custom hook
 
== Display Elements ==
 
<syntaxhighlight lang="javascript">
 
const StarsDisplay = props => (
  <>
    {utils.range(1, props.count).map(starId => (
      <div key={starId} className="star" />
    ))}
  </>
);
 
const PlayNumber = props => (
  <button
    className="number"
    style={{backgroundColor: colors[props.status]}}
    onClick={() => props.onClick(props.number, props.status)}
  >
    {props.number}
  </button>
);
 
const PlayAgain = props => (
<div className="game-done">
  <div
    className="message"
      style={{ color: props.gameStatus === 'lost' ? 'red' : 'green'}}
    >
    {props.gameStatus === 'lost' ? 'Game Over' : 'Nice'}
  </div>
  <button onClick={props.onClick}>Play Again</button>
</div>
);
</syntaxhighlight>
 
== Custom Hook ==
 
Here is the custom hook which abstracts the state and returns what is necessary for the display element of the game
 
<syntaxhighlight lang="javascript">
 
const useGameState = timeLimit => {
  const [stars, setStars] = useState(utils.random(1, 9));
  const [availableNums, setAvailableNums] = useState(utils.range(1, 9));
  const [candidateNums, setCandidateNums] = useState([]);
  const [secondsLeft, setSecondsLeft] = useState(10);
 
  useEffect(() => {
    if (secondsLeft > 0 && availableNums.length > 0) {
      const timerId = setTimeout(() => setSecondsLeft(secondsLeft - 1), 1000);
      return () => clearTimeout(timerId);
    }
  });
 
  const setGameState = (newCandidateNums) => {
    if (utils.sum(newCandidateNums) !== stars) {
setCandidateNums(newCandidateNums);
    } else {
      const newAvailableNums = availableNums.filter(
        n => !newCandidateNums.includes(n)
      );
      setStars(utils.randomSumIn(newAvailableNums, 9));
      setAvailableNums(newAvailableNums);
      setCandidateNums([]);
    }
  };
 
  return { stars, availableNums, candidateNums, secondsLeft, setGameState };
};
 
</syntaxhighlight>
 
 
== The Game ==
 
<syntaxhighlight lang="javascript">
 
const Game = props => {
  const {
    stars,
    availableNums,
    candidateNums,
    secondsLeft,
    setGameState,
  } = useGameState();
 
  const candidatesAreWrong = utils.sum(candidateNums) > stars;
  const gameStatus = availableNums.length === 0
  ? 'won'
    : secondsLeft === 0 ? 'lost' : 'active'
 
  const numberStatus = number => {
    if (!availableNums.includes(number)) {
      return 'used';
    }
 
    if (candidateNums.includes(number)) {
      return candidatesAreWrong ? 'wrong' : 'candidate';
    }
 
    return 'available';
  };
 
  const onNumberClick = (number, currentStatus) => {
    if (currentStatus === 'used' || secondsLeft === 0) {
      return;
    }
 
    const newCandidateNums =
      currentStatus === 'available'
        ? candidateNums.concat(number)
        : candidateNums.filter(cn => cn !== number);
 
    setGameState(newCandidateNums);
  };
 
  return (
    <div className="game">
      <div className="help">
        Pick 1 or more numbers that sum to the number of stars
      </div>
      <div className="body">
        <div className="left">
          {gameStatus !== 'active' ? (
          <PlayAgain onClick={props.startNewGame} gameStatus={gameStatus} />
          ) : (
          <StarsDisplay count={stars} />
          )}
        </div>
        <div className="right">
          {utils.range(1, 9).map(number => (
            <PlayNumber
              key={number}
              status={numberStatus(number)}
              number={number}
              onClick={onNumberClick}
            />
          ))}
        </div>
      </div>
      <div className="timer">Time Remaining: {secondsLeft}</div>
    </div>
  );
};
 
const StarMatch = () => {
const [gameId, setGameId] = useState(1);
return <Game key={gameId} startNewGame={() => setGameId(gameId + 1)}/>;
}
 
== Utilities ==
 
// Color Theme
const colors = {
  available: 'lightgray',
  used: 'lightgreen',
  wrong: 'lightcoral',
  candidate: 'deepskyblue',
};
 
// Math science
const utils = {
  // Sum an array
  sum: arr => arr.reduce((acc, curr) => acc + curr, 0),
 
  // create an array of numbers between min and max (edges included)
  range: (min, max) => Array.from({length: max - min + 1}, (_, i) => min + i),
 
  // pick a random number between min and max (edges included)
  random: (min, max) => min + Math.floor(Math.random() * (max - min + 1)),
 
  // Given an array of numbers and a max...
  // Pick a random sum (< max) from the set of all available sums in arr
  randomSumIn: (arr, max) => {
    const sets = [[]];
    const sums = [];
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0, len = sets.length; j < len; j++) {
        const candidateSet = sets[j].concat(arr[i]);
        const candidateSum = utils.sum(candidateSet);
        if (candidateSum <= max) {
          sets.push(candidateSet);
          sums.push(candidateSum);
        }
      }
    }
    return sums[utils.random(0, sums.length - 1)];
  },
};
 
ReactDOM.render(<StarMatch />, mountNode);
 
</syntaxhighlight>
 
=Type Script=
This is another way to valid props
 
<syntaxhighlight lang="javascript">
 
import * as React from "react";
 
interface Props {
  name: string
}
 
function Greeting(props : Props) {
    return (
        <h1>Hello {props.name}</h1>
    )
}
 
</syntaxhighlight>


=Lifecycle=
=Lifecycle=

Latest revision as of 01:45, 12 July 2021

Quick Reference

Development Setup

A good resource for this can be found https://jscomplete.com/learn/1rd-reactful' by Samer Buna
A template for a project can be found https://github.com/jscomplete/rgs-template

Production Setup

Below are examples of dev vs production scripts

Packages build scripts

Additional changes to NPM build scripts for production

    "test:ci": "jest",
    "clean:build": "rimraf ./build && mkdir build",
    "prebuild": "run-p clean:build test:ci",
    "build": "webpack --config webpack.config.prod.js",
    "postbuild": "run-p start:api serve:build",
    "serve:build": "http-server ./build"

Webpack differences

Production

const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const webpackBundleAnalyzer = require("webpack-bundle-analyzer");

process.env.NODE_ENV = "production";

module.exports = {
  mode: "production",
  target: "web",
  devtool: "source-map",
  entry: "./src/index",
  output: {
    path: path.resolve(__dirname, "build"),
    publicPath: "/",
    filename: "bundle.js"
  },
  plugins: [
    // Display bundle stats
    new webpackBundleAnalyzer.BundleAnalyzerPlugin({ analyzerMode: "static" }),

    new MiniCssExtractPlugin({
      filename: "[name].[contenthash].css"
    }),

    new webpack.DefinePlugin({
      // This global makes sure React is built in prod mode.
      "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
      "process.env.API_URL": JSON.stringify("http://localhost:3001")
    }),
    new HtmlWebpackPlugin({
      template: "src/index.html",
      favicon: "src/favicon.ico",
      minify: {
        // see https://github.com/kangax/html-minifier#options-quick-reference
        removeComments: true,
        collapseWhitespace: true,
        removeRedundantAttributes: true,
        useShortDoctype: true,
        removeEmptyAttributes: true,
        removeStyleLinkTypeAttributes: true,
        keepClosingSlash: true,
        minifyJS: true,
        minifyCSS: true,
        minifyURLs: true
      }
    })
  ],
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ["babel-loader", "eslint-loader"]
      },
      {
        test: /(\.css)$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              sourceMap: true
            }
          },
          {
            loader: "postcss-loader",
            options: {
              plugins: () => [require("cssnano")],
              sourceMap: true
            }
          }
        ]
      }
    ]
  }
};

Development

const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

process.env.NODE_ENV = "development";

module.exports = {
  mode: "development",
  target: "web",
  devtool: "cheap-module-source-map",
  entry: "./src/index",
  output: {
    path: path.resolve(__dirname, "build"),
    publicPath: "/",
    filename: "bundle.js"
  },
  devServer: {
    stats: "minimal",
    overlay: true,
    historyApiFallback: true,
    disableHostCheck: true,
    headers: { "Access-Control-Allow-Origin": "*" },
    https: false
  },
  plugins: [
    new webpack.DefinePlugin({
      "process.env.API_URL": JSON.stringify("http://localhost:3001")
    }),
    new HtmlWebpackPlugin({
      template: "src/index.html",
      favicon: "src/favicon.ico"
    })
  ],
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ["babel-loader", "eslint-loader"]
      },
      {
        test: /(\.css)$/,
        use: ["style-loader", "css-loader"]
      }
    ]
  }
};

Source switches

// Use CommonJS require below so we can dynamically import during build-time.
if (process.env.NODE_ENV === "production") {
  module.exports = require("./configureStore.prod");
} else {
  module.exports = require("./configureStore.dev");
}

Variables

 const myVar = (
   <h1>Love it</h1>
 );

Common Errors

When using classes and you have forgotten to bind you get

Cannot read property 'setState' of undefined

This is because you have not bound the function in the class to this e.g.

this.onTitleChange = this.onTitleChange.bind(this);

Form Example

Classes

Below is the old class approach with binding and constructor

class InputPage extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      course : {
        title: ""
      }
    };

    this.onTitleChange = this.onTitleChange.bind(this);
  }

  onTitleChange(event) {
    this.setState({
      course: {
        title: event.target.value,
      },
    });
  }

  render() {
    return (
      <form >
        <input
          type="text"
          value={this.state.course.title}
          onChange={this.onTitleChange}
        />
      </form>
    );
  }
}

And now old class approach without binding and constructor and using spread

class InputPage extends React.Component {

    this.state = {
      course : {
          title: ""
      }
    }

    onTitleChange = event => {
        const course = {...this.state.course, title: event.target.value}
        this.setState( {course} )
    }
  
    render() {
        return (
        <form >
            <input
            type="text"
            value={this.state.course.title}
            onChange={this.onTitleChange}
            />
         </form>
    )
}

Hooks

Hooks can only be used at the top level. You can set rules in your editor to ensure this.

UseState

Example of setting state

const InputElement = () => {

    const [inputText, setInputText] = useState("")
    
    return ( 
        <input placeholder="some text" onChange={(event) => { setInputText(event.target.value) } } />
    )
}

UseRef

You can get a reference from the DOM using UseRef

    const imageRef = useRef(null)

    console.info("Got here")

    return (
        <img 
            onMouseOver={ ()=>{ imageRef.current.src = secondaryImg} }
            onMouseOut={ ()=>{ imageRef.current.src = primaryImg} }
            src={primaryImg}
            ref={imageRef}
        />
        )

UseEffect

The first argument to useEffect is a function which executes when the component first mounts. The first argument of the return should be a function which remove the resources, if applicable, used as the first argument. e.g.
Add and remove scroll handler

    useEffect( () = > {
       window.addEventListener("scroll", scrollHandler)
       return ( ()=> {
           window.removeEventListener("scroll", scrollHandler)
       })
    })

The second argument is a array of values which, if changed, will cause the effect to be called again.

e.g. Maybe a checkbox value

    const [checkboxValue, setCheckboxValue] = useState(false)

    useEffect( () = > {
       window.addEventListener("scroll", scrollHandler)
       return ( ()=> {
           window.removeEventListener("scroll", scrollHandler)
       }, [checkboxValue])
    })

The effect will be called each time the checkbox is clicked.

UseConfig

This allows passing of config to different pages.

Top Level, wraps a context around pages

    import React from "react";
    import Home from "./Home";
    import Speakers from "./Speakers";

    export const AppContext = React.createContext();

    const pageToShow = pageName => {
      if (pageName === "Home") return <Home />;
      if (pageName === "Speakers") return <Speakers />;
      return <div>Not Found</div>;
    };

    const configValue = {
      showSpeakerSpeakingDays : true
    }

    const App = ({ pageName }) => {
      return  <AppContext.Provider value={configValue}>
                <div>{pageToShow(pageName)}</div>
              </AppContext.Provider>
    };

    export default App;

A the usage on the page.

    import React, { useState, useEffect, useContext} from "react";
    import  { AppContext } from "./App"

    const PageFunction = ({}) => {
       const context = useContext(AppContext)
    }

UseReducer

A reducer is defined as something which takes a previous state and function and returns a new state

(previousState, action) => newState


UseState is built on Reducer i.e.

    const [speakerList, setSpeakerList] = useState([])

Is the same as

    const [speakerList, setSpeakerList] = useReducer( (state,action) => action, [])

Is the same as

    function speakerReducer(state, action) {
        return action
    }

    const [speakerList, setSpeakerList] = useReducer( speakerReducer, [])

We can now make this work the same a the useState

    function speakerReducer(state, action) {
        switch(action.type) {
            case "setSpeakerList" : {
                return action.data;
            }
            default: 
                return state;
        }
    }

    // So replaced setSpeakList, which is not returned with a dispatch name, we called it dispatch but we could have called derek

    // const [speakerList, setSpeakerList] = useReducer( speakerReducer, [])
    const [speakerList, dispatch] = useReducer( speakerReducer, [])

So the old usage was

    setSpeakerList(mySpeakerList)

Using the reducer gives

    dispatch( {
        type: "setSpeakerList",
        data: mySpeakerList 
    })

UseCallback

This caches a function. Given the function below,

  const heartFavoriteHandler = (e, favoriteValue) => {
    e.preventDefault();
    const sessionId = parseInt(e.target.attributes["data-sessionid"].value);
    
    dispatch({
      type : favoriteValue === true ? "setFavourite" : "setUnfavourite",
      sessionId
    });

We can cache this using UseCallback

  const heartFavoriteHandler = useCallback( (e, favoriteValue) => {
    e.preventDefault();
    const sessionId = parseInt(e.target.attributes["data-sessionid"].value);
    
    dispatch({
      type : favoriteValue === true ? "setFavourite" : "setUnfavourite",
      sessionId
    }, [] );

However for this to work you will need to wrap the content it is reloading in React.memo. In this case it was SpeakerDetail

          <div className="card-deck">
            {speakerListFiltered.map(
              ({ id, firstName, lastName, bio, favorite }) => {
                return (
                  <SpeakerDetail
                    key={id}
                    id={id}
                    favorite={favorite}
                    onHeartFavoriteHandler={heartFavoriteHandler}
                    firstName={firstName}
                    lastName={lastName}
                    bio={bio}
                  />
                );
              }
            )}
          </div>

so this gave

const SpeakerDetail = React.memo( ({
                           id,
                           firstName,
                           lastName,
                           favorite,
                           bio,
                           onHeartFavoriteHandler
                       }) => {
    console.log(`SpeakerDetail:${id} ${firstName} ${lastName} ${favorite}`);
    return (
        <div className="card col-4 cardmin">
            <ImageToggleOnScroll
                className="card-img-top"
                primaryImg={`/static/speakers/bw/Speaker-${id}.jpg`}
                secondaryImg={`/static/speakers/Speaker-${id}.jpg`}
                alt="{firstName} {lastName}"
            />
            <div className="card-body">
                <h4 className="card-title">
                    <button
                        data-sessionid={id}
                         const mySortedSpeakList = useMemo( ()=> speakerList 
    .filter(
      ({ sat, sun }) => (speakingSaturday && sat) || (speakingSunday && sun)
    )
    .sort(function(a, b) {
      if (a.firstName < b.firstName) {
        return -1;
      }
      if (a.firstName > b.firstName) {
        return 1;
      }
      return 0;
    }), [speakingSaturday,speakingSunday,speakerList]); className={favorite ? "heartredbutton" : "heartdarkbutton"}
                        onClick={e => {
                            onHeartFavoriteHandler(e, !favorite);
                        }}
                    />
                    <span>
            {firstName} {lastName}
          </span>
                </h4>

                <span>{bio}</span>
            </div>
        </div>
    );
});

UseMemo

Similarly you can cache a value using UseMemo. e.g.

  const mySortedSpeakerList = useMemo( ()=> speakerList 
    .filter(
      ({ sat, sun }) => (speakingSaturday && sat) || (speakingSunday && sun)
    )
    .sort(function(a, b) {
      if (a.firstName < b.firstName) {
        return -1;
      }
      if (a.firstName > b.firstName) {
        return 1;
      }
      return 0;
    }), [speakingSaturday,speakingSunday,speakerList]);

The value will only change if one of the speakingSaturday,speakingSunday,speakerList changes

Custom Hook

An example of a reducer with a custom hook using axios

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case "FETCH_INIT":
      return { ...state, isLoading: true, isError: false };
    case "FETCH_SUCCESS":
      return {
        ...state,
        isLoading: false,
        hasErrored: false,
        errorMessage: "",
        data: action.payload
      };
    case "FETCH_FAILURE":
      return {
        ...state,
        isLoading: false,
        hasErrored: true,
        errorMessage: "Data Retrieve Failure"
      };
    case "REPLACE_DATA":
      // The record passed (state.data) must have the attribute "id"
      const newData = state.data.map(rec => {
        return rec.id === action.replacerecord.id ? action.replacerecord : rec;
      });
      return {
        ...state,
        isLoading: false,
        hasErrored: false,
        errorMessage: "",
        data: newData
      };
    default:
      throw new Error();
  }
};
const useAxiosFetch = (initialUrl, initialData) => {
  const [url] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    hasErrored: false,
    errorMessage: "",
    data: initialData
  });

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      dispatch({ type: "FETCH_INIT" });

      try {
        let result = await axios.get(url);
        if (!didCancel) {
          dispatch({ type: "FETCH_SUCCESS", payload: result.data });
        }
      } catch (err) {
        if (!didCancel) {
          dispatch({ type: "FETCH_FAILURE" });
        }
      }
    };

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [url]);

  const updateDataRecord = record => {
    dispatch({
      type: "REPLACE_DATA",
      replacerecord: record
    });
  };

  return { ...state, updateDataRecord };
};

Maps

 // Only do this if items have no stable IDs
 
 const todo = ["fix", "syntax", "highlighting"];
 const todoItems = todos.map((todo, index) =>
 <li key={index}>
   {todo.text}
 </li>
);

Component Example

import React, {Component} from 'react';
import ReactDOM from 'react-dom'; 

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: "Frarthur"};
  }
  render() {
    return <div></div>;
  }  
};

Passing Props to child

Parent.json

import React from 'react';
import ReactDOM from 'react-dom';
import { Child } from './Child';

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: 'Frarthur' };
  }

  render() {
    return <Child name={this.state.name} />;
  }
}

ReactDOM.render(<Parent />, document.getElementById('app'));

Child.json

import React from 'react';

export class Child extends React.Component {
  render() {
    return <h1>Hey, my name is {this.props.name}!</h1>;
  }
}

Forms with Ref

You can capture import using the React.createRef() e.g.

class Form extends React.Component
{
   userNameInput = React.createRef();

   handleSubmit = (event) => {
     event.preventDefault()
     console.log(userNameInput.current.value)
   }
   render()
   {
       return (
           <form onSubmit={this.handleSubmit} >
               <input 
                   type="text"
                   placeHolder="background text"
                   ref={this.userNameInput}
                   required
               />
           />
       ) 
    
   }
}

Forms without Ref

A Better way may be to use a state to hold the name.

class Form extends React.Component
{
   state = { userName : ''}

   render()
   {
       return (
           <form onSubmit={this.handleSubmit} >
               <input 
                   type="text"
                   value={this.state.userName}
                   OnChange = { 
                       event => {
                           this.setState( {userName: event.target.value} )
                       } 
                   }
                   required
               />
           />
       ) 
    
   }
}

Binding a function

import React from 'react';
import ReactDOM from 'react-dom';
import { Video } from './Video';
import { Menu } from './Menu';

const VIDEOS = {
  fast: 'https://s3.amazonaws.com/codecademy-content/courses/React/react_video-fast.mp4',
  slow: 'https://s3.amazonaws.com/codecademy-content/courses/React/react_video-slow.mp4',
  cute: 'https://s3.amazonaws.com/codecademy-content/courses/React/react_video-cute.mp4',
  eek: 'https://s3.amazonaws.com/codecademy-content/courses/React/react_video-eek.mp4'
};

class App extends React.Component {
  constructor(props) {
    this.chooseVideo = this.chooseVideo.bind(this);
    super(props);
    this.state = { 
      src: VIDEOS.fast 
    };
  }
  
  chooseView(newView)   {
    this.setState({
      src: VIDEOS[newVideo]
    });
  }
  render() {
    
    return (
      <div>
        <h1>Video Player</h1>
        <Menu chooseVideo={this.chooseView()} />
        <Video src={this.state.src} />
      </div>
    );
  }
}

ReactDOM.render(
  <App />, 
  document.getElementById('app')
);

import React from 'react';

export class Menu extends React.Component {
  
  constructor() {
    super(props)
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick(e) {
    var text = e.target.value;
    this.props.chooseView(text);
  }
  
  render() {
    return (
      <form OnClick={this.handleClick}>
        <input type="radio" name="src" value="fast" /> fast
        <input type="radio" name="src" value="slow" /> slow
        <input type="radio" name="src" value="cute" /> cute
        <input type="radio" name="src" value="eek" /> eek
      </form>
    );
  }
}

import React from 'react';

export class Video extends React.Component {
  render() {
    return (
      <div>
        <video controls autostart autoPlay muted src={this.props.src} />
      </div>
    );
  }
}

Inline Styling

React needs double quotes to inject style in JSX e.g.

import React from 'react';
import ReactDOM from 'react-dom';

const styleMe = <h1 style={{ background: 'lightblue', color: 'darkred' }}>Please style me! I am so bland!</h1>;

ReactDOM.render(
	styleMe, 
	document.getElementById('app')
);

Another example

mport React from 'react';
import ReactDOM from 'react-dom';
const styles = {
  background: 'lightblue',
  color:      'darkred',
  marginTop: '100px',
  fontSize: '50px'
}

const styleMe = <h1 style={styles}>Please style me! I am so bland!</h1>;

ReactDOM.render(
	styleMe, 
	document.getElementById('app')
);

You can write px styles without quotes e.g.

const styles = {
  background: 'lightblue',
  color:      'darkred',
  marginTop: 100,
  fontSize: 50
}

Stateless Classes

A component class written in the usual way:

export class MyComponentClass extends React.Component {
  render() {
    return <h1>Hello world</h1>;
  }
}

// The same component class, written as a stateless functional component:
export const MyComponentClass = () => {
  return <h1>Hello world</h1>;
}

You can pass the properties from the container class like this

import React from 'react';

export const GuineaPigs = (props) => {
  let src = props.src;
  return (
    <div>
      <h1>Cute Guinea Pigs</h1>
      <img src={src} />
    </div>
  );
}

Prototypes

Add them at the bottom so people can see how to use the code

import React from 'react';

export class BestSeller extends React.Component {
  render() {
    return (
      <li>
        Title: <span>
          {this.props.title}
        </span><br />
        
        Author: <span>
          {this.props.author}
        </span><br />
        
        Weeks: <span>
          {this.props.weeksOnList}
        </span>
      </li>
    );
  }
}

// This propTypes object should have
// one property for each expected prop:
BestSeller.propTypes = {
  title: React.PropTypes.string.isRequired,
  author: React.PropTypes.string.isRequired,
  weeksOnList: React.PropTypes.number.isRequired,
};

And a render example

import React from 'react';
import ReactDOM from 'react-dom';
import { BestSeller } from './BestSeller';

export class BookList extends React.Component {
  render() {
    return (
      <div>
        <h1>Best Sellers</h1>
        <div>
          <ol>
            <BestSeller 
              title="Glory and War Stuff for Dads" 
              author="Sir Eldrich Van Hoorsgaard" 
              weeksOnList={10} />
            <BestSeller 
              title="The Crime Criminals!" 
              author="Brenda Sqrentun" 
              weeksOnList={2} />
            <BestSeller
              title="Subprime Lending For Punk Rockers" 
              author="Malcolm McLaren" 
              weeksOnList={600} />
          </ol>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<BookList />, document.getElementById('app'));

And for stateless functions

import React from 'react';

export class GuineaPigs extends React.Component {
  render() {
    let src = this.props.src;
    return (
      <div>
        <h1>Cute Guinea Pigs</h1>
        <img src={src} />
      </div>
    );
  }
}

GuineaPigs.propTypes = {
  src: React.PropTypes.string.isRequired
};

Form Example where we track the input value

import React from 'react';
import ReactDOM from 'react-dom';

export class Input extends React.Component {
  constructor(props) {
    super(props);
    
    this.state = { userInput: '' };
    
    this.handleUserInput = this.handleUserInput.bind(this);
  }
  
  handleUserInput(e) {
    this.setState({userInput: e.target.value});
  }

  render() {
    return (
      <div>
        <input type="text" onChange={this.handleUserInput} value={this.state.userInput} />
        <h1>{this.state.userInput}</h1>
      </div>
    );
  }
}

ReactDOM.render(
	<Input />,
	document.getElementById('app')
);

The New Way Example using custom hooks

This is the example from the react course where they broke the game into a custom hook

Display Elements

const StarsDisplay = props => (
  <>
    {utils.range(1, props.count).map(starId => (
      <div key={starId} className="star" />
    ))}
  </>
);

const PlayNumber = props => (
  <button
    className="number"
    style={{backgroundColor: colors[props.status]}}
    onClick={() => props.onClick(props.number, props.status)}
  >
    {props.number}
  </button>
);

const PlayAgain = props => (
	<div className="game-done">
  	<div 
    	className="message"
      style={{ color: props.gameStatus === 'lost' ? 'red' : 'green'}}
    >
  	  {props.gameStatus === 'lost' ? 'Game Over' : 'Nice'}
  	</div>
	  <button onClick={props.onClick}>Play Again</button>
	</div>
);

Custom Hook

Here is the custom hook which abstracts the state and returns what is necessary for the display element of the game

const useGameState = timeLimit => {
  const [stars, setStars] = useState(utils.random(1, 9));
  const [availableNums, setAvailableNums] = useState(utils.range(1, 9));
  const [candidateNums, setCandidateNums] = useState([]);
  const [secondsLeft, setSecondsLeft] = useState(10);

  useEffect(() => {
    if (secondsLeft > 0 && availableNums.length > 0) {
      const timerId = setTimeout(() => setSecondsLeft(secondsLeft - 1), 1000);
      return () => clearTimeout(timerId);
    }
  });

  const setGameState = (newCandidateNums) => {
    if (utils.sum(newCandidateNums) !== stars) {
			setCandidateNums(newCandidateNums);
    } else {
      const newAvailableNums = availableNums.filter(
        n => !newCandidateNums.includes(n)
      );
      setStars(utils.randomSumIn(newAvailableNums, 9));
      setAvailableNums(newAvailableNums);
      setCandidateNums([]);
    }
  };

  return { stars, availableNums, candidateNums, secondsLeft, setGameState };
};


The Game

const Game = props => {
  const {
    stars,
    availableNums,
    candidateNums,
    secondsLeft,
    setGameState,
  } = useGameState();

  const candidatesAreWrong = utils.sum(candidateNums) > stars;
  const gameStatus = availableNums.length === 0 
  	? 'won'
    : secondsLeft === 0 ? 'lost' : 'active'

  const numberStatus = number => {
    if (!availableNums.includes(number)) {
      return 'used';
    }

    if (candidateNums.includes(number)) {
      return candidatesAreWrong ? 'wrong' : 'candidate';
    }

    return 'available';
  };

  const onNumberClick = (number, currentStatus) => {
    if (currentStatus === 'used' || secondsLeft === 0) {
      return;
    }

    const newCandidateNums =
      currentStatus === 'available'
        ? candidateNums.concat(number)
        : candidateNums.filter(cn => cn !== number);

    setGameState(newCandidateNums);
  };

  return (
    <div className="game">
      <div className="help">
        Pick 1 or more numbers that sum to the number of stars
      </div>
      <div className="body">
        <div className="left">
          {gameStatus !== 'active' ? (
          	<PlayAgain onClick={props.startNewGame} gameStatus={gameStatus} />
          ) : (
          	<StarsDisplay count={stars} />
          )}
        </div>
        <div className="right">
          {utils.range(1, 9).map(number => (
            <PlayNumber
              key={number}
              status={numberStatus(number)}
              number={number}
              onClick={onNumberClick}
            />
          ))}
        </div>
      </div>
      <div className="timer">Time Remaining: {secondsLeft}</div>
    </div>
  );
};

const StarMatch = () => {
	const [gameId, setGameId] = useState(1);
	return <Game key={gameId} startNewGame={() => setGameId(gameId + 1)}/>;
}

== Utilities ==

// Color Theme
const colors = {
  available: 'lightgray',
  used: 'lightgreen',
  wrong: 'lightcoral',
  candidate: 'deepskyblue',
};

// Math science
const utils = {
  // Sum an array
  sum: arr => arr.reduce((acc, curr) => acc + curr, 0),

  // create an array of numbers between min and max (edges included)
  range: (min, max) => Array.from({length: max - min + 1}, (_, i) => min + i),

  // pick a random number between min and max (edges included)
  random: (min, max) => min + Math.floor(Math.random() * (max - min + 1)),

  // Given an array of numbers and a max...
  // Pick a random sum (< max) from the set of all available sums in arr
  randomSumIn: (arr, max) => {
    const sets = [[]];
    const sums = [];
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0, len = sets.length; j < len; j++) {
        const candidateSet = sets[j].concat(arr[i]);
        const candidateSum = utils.sum(candidateSet);
        if (candidateSum <= max) {
          sets.push(candidateSet);
          sums.push(candidateSum);
        }
      }
    }
    return sums[utils.random(0, sums.length - 1)];
  },
};

ReactDOM.render(<StarMatch />, mountNode);

Type Script

This is another way to valid props

 import * as React from "react";

 interface Props {
   name: string
 }

 function Greeting(props : Props) {
    return (
        <h1>Hello {props.name}</h1>
    )
 }

Lifecycle

A component “mounts” when it renders for the first time. This is when mounting lifecycle methods get called.

There are three mounting lifecycle methods:

  • componentWillMount
  • render
  • componentDidMount

There are five updating lifecycle methods:

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

Some words of wisdom

A React component should use props to store information that can be changed, but can only be changed by a different component.

A React component should use state to store information that the component itself can change.