Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React State

Steve Kinney
November 21, 2019
10k

React State

Steve Kinney

November 21, 2019
Tweet

Transcript

  1. So, what are we going to do today? • Think

    deeply about what “state” even means in a React application. • Learn a bit about the inner workings of this.setState. • How class-based component state and hooks differ. • Explore APIs for navigating around prop-drilling. • Use reducers for advanced state management.
  2. So, what are we going to do today? • Write

    our own custom hooks for managing state. • Store state in Local Storage. • Store state in the URL using query parameters. • Fetch state from a server—because that’s a thing.
  3. The main job of React is to take your application

    state and turn it into DOM nodes.
  4. There are many kinds of state. • Model data: The

    nouns in your application. • View/UI state: Are those nouns sorted in ascending or descending order? • Session state: Is the user even logged in? • Communication: Are we in the process of fetching the nouns from the server? • Location: Where are we in the application? Which nouns are we looking at?
  5. Or, it might make sense to think about state relative

    to time. • Model state: This is likely the data in your application. This could be the items in a given list. • Ephemeral state: Stuff like the value of an input field that will be wiped away when you hit “enter.” This could be the order in which a given list is sorted.
  6. Exercise • Okay, this is going to be a quick

    one to get warmed up. • https://github.com/stevekinney/simple-counter-react-state • It will have three buttons: • Increment • Decrement • Reset • Your job: Get decrement and reset working. • Only touch <Counter !/> for now.
  7. this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count +

    1 }); this.setState({ count: this.state.count + 1 }); console.log(this.state.count);
  8. 0

  9. export default class Counter extends Component { constructor() { super();

    this.state = { count: 0 }; this.increment = this.increment.bind(this); } increment() {…} render() { return ( <section> <h1>Count: {this.state.count}!</h1> <button onClick={this.increment}>Increment!</button> !</section> ) } }
  10. export default class Counter extends Component { constructor() { …

    } increment() { this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); } render() { … } }
  11. 1

  12. React will batch them up, figure out the result and

    then efficiently make that change.
  13. Fun fact: Did you know that you can also pass

    a function in as an argument?
  14. import React, { Component } from 'react'; export default class

    Counter extends Component { constructor() { … } increment() { this.setState((state) !=> { return { count: state.count + 1 } }); this.setState((state) !=> { return { count: state.count + 1 } }); this.setState((state) !=> { return { count: state.count + 1 } }); } render() { … } }
  15. 3

  16. import React, { Component } from 'react'; export default class

    Counter extends Component { constructor() { … } increment() { this.setState(state !=> { if (state.count !>= 5) return; return { count: state.count + 1 }; }) } render() { … } }
  17. import React, { Component } from 'react'; export default class

    Counter extends Component { constructor() { … } increment() { this.setState( { count: this.state.count + 1 }, () !=> { console.log(this.state); } ) } render() { … } }
  18. class User extends Component { constructor(props) { super(props); this.state =

    { fullName: props.firstName + ' ' + props.lastName }; } }
  19. class User extends Component { render() { const { firstName,

    lastName } = this.props; const fullName = firstName + ' ' + lastName; return ( <h1>{fullName}!</h1> ); } }
  20. !// Alternatively… class User extends Component { get fullName() {

    const { firstName, lastName } = this.props; return firstName + ' ' + lastName; } render() { return ( <h1>{this.fullName}!</h1> ); } }
  21. class UserList extends Component { render() { const { users

    } = this.props; return ( <section> <VeryImportantUserControls !/> { users.map(user !=> ( <UserProfile key={user.id} photograph={user.mugshot} onLayoff={handleLayoff} !/> )) } <SomeSpecialFooter !/> !</section> ); } }
  22. class UserList extends Component { renderUserProfile(user) { return ( <UserProfile

    key={user.id} photograph={user.mugshot} onLayoff={handleLayoff} !/> ) } render() { const { users } = this.props; return ( <section> <VeryImportantUserControls !/> { users.map(this.renderUserProfile) } <SomeSpecialFooter !/> !</section> ); } }
  23. const renderUserProfile = user !=> { return ( <UserProfile key={user.id}

    photograph={user.mugshot} onLayoff={handleLayoff} !/> ); }; const UserList = ({ users }) !=> { return ( <section> <VeryImportantUserControls !/> {users.map(renderUserProfile)} <SomeSpecialFooter !/> !</section> ); };
  24. class TweetStream extends Component { constructor() { super(); this.state =

    { tweets: [], tweetChecker: setInterval(() !=> { Api.getAll('/api/tweets').then(newTweets !=> { const { tweets } = this.state; this.setState({ tweets: [ !!...tweets, newTweets ] }); }); }, 10000) } } componentWillUnmount() { clearInterval(this.state.tweetChecker); } render() { !// Do stuff with tweets } }
  25. class TweetStream extends Component { constructor() { super(); this.state =

    { tweets: [], } } componentWillMount() { this.tweetChecker = setInterval( … ); } componentWillUnmount() { clearInterval(this.tweetChecker); } render() { !// Do stuff with tweets } }
  26. class Items extends Component { constructor() { super(); } componentDidMount()

    { Api.getAll('/api/items').then(items !=> { this.setState({ items }); }); } render() { !// Do stuff with items } }
  27. class Items extends Component { constructor() { super(); this.state =

    { items: [] } } componentDidMount() { Api.getAll('/api/items').then(items !=> { this.setState({ items }); }); } render() { !// Do stuff with items } }
  28. const [count, setCount] = React.useState(0); const increment = () !=>

    setCount(count + 1); const decrement = () !=> setCount(count - 1); const reset = () !=> setCount(0);
  29. const increment = () !=> { setCount(c !=> c +

    1); setCount(c !=> c + 1); setCount(c !=> c + 1); };
  30. Exercise • Can you add a second effect that updates

    the document’s title whenever the count changes? • (Hint: document.title is your friend here.)
  31. const countRef = React.useRef(); countRef.current = count; React.useEffect(() !=> {

    setTimeout(() !=> { console.log(`You clicked ${countRef.current} times`); }, 3000); }, [count]);
  32. useEffect(() !=> { let x; const id = setInterval(() !=>

    { console.log(x!++); }, 3000); return () !=> { clearInterval(x!++); } });
  33. What’s the deal with useReducer()? • So, it turns out

    that it has nothing to do with Redux. • But, it does allow you to use reducers—just like Redux. • The cool part is that it allows you to create interfaces where you (or a friend) can pass in the mechanics about how to update state.
  34. Exercise • Be a better person than me. • I’ve

    implemented the ability to add a grudge. • Can you implement the ability to forgive one?
  35. Cards Cards Card Card Application Boards New Board Users New

    User Board Board Board Cards Card Card Card Card User User Move to Board Assign to User Move to Board Assign to User Move to Board Assign to User Move to Board Assign to User Move to Board Assign to User Move to Board Assign to User
  36. “ Context provides a way to pass data through the

    component tree without having to pass props down manually at every level.
  37. const CountContext = createContext(); class CountProvider extends Component { state

    = { count: 0 }; increment = () !=> this.setState(({ count }) !=> ({ count: count + 1 })); decrement = () !=> this.setState(({ count }) !=> ({ count: count - 1 })); render() { const { increment, decrement } = this; const { count } = this.state; const value = { count, increment, decrement }; return ( <CountContext.Provider value={value}> {this.props.children} !</CountContext.Provider> ); } }
  38. Some Tasting Notes • We lost all of our performance

    optimizations when moving to the Context API. • What’s the right answer? It’s a trade off. • Grudge List might seem like a toy application, but it could also represent a smaller part of a larger system. • Could you use the Context API to get things all of the way down to this level and then use the approach we had previously?
  39. const board = { lists: [ { id: '1558196567543', title:

    'Backburner', cards: [ { id: '1558196597470', title: 'Learn to Normalize Data', description: 'Iterating through arrays is rough business', }, !// … ], }, !// … ], };
  40. const board = { lists: { '1558196567543': { title: 'Backburner',

    cards: ['1558196597470'], }, !// … }, cards: { '1558196597470': { title: 'Learn to Normalize State', description: 'This is much better.', assignedTo: '1', }, !// … }, users: { '1': { name: 'Steve Kinney', }, }, };
  41. const removeCard = (listId, cardId) !=> { const targetList =

    lists.find(list !=> listId !!=== list.id); const remainingCards = targetList.cards.filter(({ id }) !=> id !!!== cardId); const updatedList = { !!...targetList, cards: remainingCards }; const updatedLists = lists.map(list !=> { return list.id !!=== listId ? updatedList : list; }); setLists(updatedLists); };
  42. const removeCard = (listId, cardId) !=> { const list =

    this.state.lists[listId]; const cards = omit(this.cards, cardId); const lists = { !!...this.state.lists, [listId]: { !!...list, cards: list.cards.filter(card !=> card.id !!=== cardId), }, }; this.setState({ lists, cards }); };
  43. const board = { lists: { '1558196567543': { title: 'Backburner',

    cards: ['1558196597470'], }, !// … }, cards: { '1558196597470': { title: 'Learn to Normalize State', description: 'This is much better.', assignedTo: '1', }, !// … }, users: { '1': { name: 'Steve Kinney', }, }, };
  44. Moral of the Story • Being thoughtful about how you

    structure your state can have implications for the maintainability of your application. • It can also have some implications for performance as well. • Modern versions of React come with tools for eliminating some of the pain points that used to require third party libraries.
  45. Exercise • Can you factor this out into a useFetch

    hook? • It should take an endpoint as an argument. • It should return response, loading, and error. • You might want the following in order to get the right data back out. • const characters = (response !&& response.characters) !|| [];
  46. The major idea behind a thunk is that it is

    code to be executed later.
  47. export const getAllItems = () !=> { return dispatch !=>

    { Api.getAll().then(items !=> { dispatch({ type: UPDATE_ALL_ITEMS, items, }); }); }; };
  48. Exercise • There is another component called CharacterSearch. • It

    would be cool if we could use the api/search/:query endpoint to get all of the characters that match whatever is typed in that bar. • Can you implement that?
  49. Why keep state in the route? • It’s an established

    pattern that predates most of what we’re doing in the browser these days. • It allows our users to save and share the URL with all of the state baked right in.