Giter Site home page Giter Site logo

stevekinney / simple-counter Goto Github PK

View Code? Open in Web Editor NEW
6.0 4.0 4.0 4.36 MB

An example application for Steve's React, Redux, and MobX workshops.

Home Page: https://codesandbox.io/s/github/stevekinney/simple-counter

HTML 57.63% JavaScript 17.88% CSS 24.49%

simple-counter's Introduction

From Component State to Hooks

We're going to start with a super simple counter (edit on CodeSandbox).

Out of the box, it doesn't have a lot going on.

class Counter extends Component {
  render() {
    return (
      <main className="Counter">
        <p className="count">0</p>
        <section className="controls">
          <button>Increment</button>
          <button>Decrement</button>
          <button>Reset</button>
        </section>
      </main>
    );
  }
}

Let's get it wired up as a fun warmup exercise.

Getting the Basic Component Wired Up

We'll start with a constructor method that sets the component state.

constructor(props) {
  super(props);
  this.state = {
    count: 0,
  };
}

We'll use that state in the component.

render() {
  const { count } = this.state;

  return (
    <main className="Counter">
      <p className="count">{count}</p>
      <section className="controls">
        <button>Increment</button>
        <button>Decrement</button>
        <button>Reset</button>
      </section>
    </main>
  );
}

Alright, now we'll implement the methods to incrementm, decrement, and reset the count.

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

decrement() {
  this.setState({ count: this.state.count - 1 });
}

reset() {
  this.setState({ count: 0 });
}

We'll add those methods to the buttons.

<button onClick={this.increment}>Increment</button>
<button onClick={this.decrement}>Decrement</button>
<button onClick={this.reset}>Reset</button>

We need to bind those event listeners because everything is terrible.

constructor(props) {
  super(props);
  this.state = {
    count: 3,
  };

  this.increment = this.increment.bind(this);
  this.decrement = this.decrement.bind(this);
  this.reset = this.reset.bind(this);
}

Component State Pop Quiz

Asyncronous Updates and Queuing

Okay, let's say we refactored increment() as follows:

increment() {
  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);
}

Two questions:

  1. What will be logged to the console?
  2. What will the new value be?

Using a Function as an Argument

this.setState also takes a function. This means we could refactor increment() as follows.

this.setState(state => {
  return { count: state.count + 1 };
});

If we wanted to show off, we could use destructuring to make it evening cleaner.

increment() {
  this.setState(({ count }) => {
    return { count: count + 1 };
  });
}

There are some potentally cool things we could do here. For example, we could add some logic to our component.

this.setState((state, props) => {
  if (state.count >= 10) return;
  return { count: state.count + 1 };
});

Let's stay, we wanted to add in a maximum count as a prop.

render(<Counter max={10} />, document.getElementById('root'));
this.setState(state => {
  if (state.count >= this.props.max) return;
  return { count: state.count + 1 };
});

It turns out that we can actually have a second argument in there as well—the props.

this.setState((state, props) => {
  if (state.count >= props.max) return;
  return { count: state.count + 1 };
});

Oh wait—what if we did this three times?

increment() {
  this.setState((state, props) => {
    if (state.count >= props.max) return;
    return { count: state.count + 1 };
  });
  this.setState((state, props) => {
    if (state.count >= props.max) return;
    return { count: state.count + 1 };
  });
  this.setState((state, props) => {
    if (state.count >= props.max) return;
    return { count: state.count + 1 };
  });
}

Oh, that's interesting.

The other thing we can do is pull that function out of the component. This makes it way easier to unit test.

const increment = (state, props) => {
  if (state.count >= props.max) return;
  return { count: state.count + 1 };
};
increment() {
  this.setState(increment);
}

Callbacks

this.setState takes a second argument in addition to either the object or function. This function is called after the state change has happened.

Here is the simplest possible implementation.

this.setState(increment, () => console.log('Callback'));

We can also do something like.

this.setState(increment, () => console.log(this.state));

Here is a simple thing we might choose to do.

this.setState(increment, () => (document.title = `Count: ${this.state.count}`));

Using LocalStorage in a Side Effect

Let's make a little helper function.

const getStateFromLocalStorage = () => {
  const storage = localStorage.getItem('counterState');
  if (storage) return JSON.parse(storage);
  return { count: 0 };
};

We can then use a callback to set localStorage when the state changes.

this.setState(increment, () =>
  localStorage.setItem('counterState', JSON.stringify(this.state))
);

Let's pull that out along with increment.

const storeStateInLocalStorage = () => {
  localStorage.setItem('counterState', JSON.stringify(this.state));
};
increment() {
  this.setState(increment, storeStateInLocalStorage);
}

It doesn't work. It's a bummer. It would be great if the callback function got a copy of the state, but it doesn't. We could wrap it into a function and then pass the state in.

We could handle this a few ways.

We could use an anonymous function and then pass it in as an argument.

const storeStateInLocalStorage = state => {
  localStorage.setItem('counterState', JSON.stringify(state));
};
increment() {
  this.setState(increment, () => storeStateInLocalStorage(this.state));
}

Alternatively, if we're willing to give up on arrow functions, we can use bind.

function storeStateInLocalStorage() {
  localStorage.setItem('counterState', JSON.stringify(this.state));
}
increment() {
  this.setState(increment, storeStateInLocalStorage.bind(this));
}

Lastly, we can just put it onto the class component itself.

storeStateInLocalStorage() {
  localStorage.setItem('counterState', JSON.stringify(this.state));
}

increment() {
  this.setState(increment, this.storeStateInLocalStorage);
}

This is probably your best bet.

Refactoring to Hooks

Hooks are a new pattern that allow us to write a lot less code. Get ready to delete some code.

Let's start by deleting everything but the render method.

const Counter = ({ max }) => {
  const [count, setCount] = React.useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(0);

  return (
    <main className="Counter">
      <p className="count">{count}</p>
      <section className="controls">
        <button onClick={increment}>Increment</button>
        <button onClick={decrement}>Decrement</button>
        <button onClick={reset}>Reset</button>
      </section>
    </main>
  );
};

Running Some of Our Previous Experiments

What if we tripled up again?

const increment = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};

Okay, so that makes sense.

It also turns out that useState setters can take functions too.

const increment = () => {
  setCount(c => c + 1);
};

Unlike using values, using functions also works the same way as it does with this.setState.

const increment = () => {
  setCount(c => c + 1);
  setCount(c => c + 1);
  setCount(c => c + 1);
};

There is an important difference. You get only the state in this case. There is no second argument with the props. That said, we have them in scope.

They also do not support callback functions like this.setState. Later on, we'll use useEffect to trigger side effects based on state changes.

Earlier with this.setState, we ended up returning undefined if our count had hit the max. What if we did something similar here?

setCount(c => {
  if (c >= max) return;
  return c + 1;
});

Oh—it explodes after ten. This is core to the difference between how useState and this.setState works.

With this.setState, we're giving the component that object of values that it needs to update. With useState, we've got a dedicated function to change a particular piece of state.

How can we fix this?

setCount(c => {
  if (c >= max) return c;
  return c + 1;
});

A Brief Introduction to useEffect

We're going to go a bit deeper into useEffect, but let's do the high level now.

Use effect allows us to implement some kind of side effect in our component outside of the changes to state and props triggering a new render.

This is useful for a ton of reasons:

  • Storing stuff in localStorage.
  • Making AJAX requests.

Implementing LocalStorage

Let's get the basic set up in place here.

Here is a reminder of that function of getting from localStorage.

const getStateFromLocalStorage = () => {
  const storage = localStorage.getItem('counterState');
  if (storage) return JSON.parse(storage);
  return { count: 0 };
};

We'll read the count property from localStorage.

const [count, setCount] = React.useState(getStateFromLocalStorage().count);

Now, we'll register a side effect.

React.useEffect(() => {
  localStorage.setItem('counterState', JSON.stringify({ count }));
}, [count]);

The coolest part about this is that it works for increment, decrement, and reset all at once.

Quick Exercise

Register an effect that updates the document title.

Pulling It Out Into a Custom Hook

const getStateFromLocalStorage = (defaultValue, key) => {
  const storage = localStorage.getItem(key);
  if (storage) return JSON.parse(storage).value;
  return defaultValue;
};

const useLocalStorage = (defaultValue, key) => {
  const initialValue = getStateFromLocalStorage(defaultValue, key);
  const [value, setValue] = React.useState(initialValue);

  React.useEffect(() => {
    localStorage.setItem(key, JSON.stringify({ value }));
  }, [value]);

  return [value, setValue];
};

Now, we just never think about it again.

const [count, setCount] = useLocalStorage(0, 'count');

Understanding the Differences Between Class Components

Okay, now—let's switch over to the class-based implementation in component-state-completed.

We'll add this:

componentDidUpdate() {
  setTimeout(() => {
    console.log(`Count: ${this.state.count}`);
  }, 3000);
}

The delay is intended to just create some space between the click and what we long to the console

Let's switch to a Hooks-based component on the hooks branch.

React.useEffect(() => {
  setTimeout(() => {
    console.log(`Count: ${count}`);
  }, 3000);
}, [count]);
```.

That's a much different result, isn't it?

Could we implement the older functionality with this newer syntax?

```js
const countRef = React.useRef();
countRef.current = count;

React.useEffect(() => {
  setTimeout(() => {
    console.log(`You clicked ${countRef.current} times`);
  }, 3000);
}, [count]);

This is actualy persisted between renders.

This pattern can be useful if you need to know about the previous state of the the component.

const countRef = React.useRef();
let message = '';

if (countRef.current < count) message = 'Higher';
if (countRef.current > count) message = 'Lower';

countRef.current = count;

simple-counter's People

Contributors

1marc avatar stevekinney avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.