Skip to content

dariaoldenburg/au-react-workshop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AppUnite React Workshops no 1

Let's do a PolChat!

PolChat screenshot

Demo app: https://dariaoldenburg.github.io/au-react-workshop/

Development

yarn
npm start
# Open http://localhost:3000

Testing

# Launches the test runner in the interactive watch mode.
# See https://facebook.github.io/create-react-app/docs/running-tests for more information.
npm test

Lessons

Learning resources

Lesson 1

ReactDOM.render()

Docs

// JS
const app = React.createElement(App);
ReactDOM.render(App, document.getElementById('root'));

// JSX
const app = <App />;
ReactDOM.render(app, document.getElementById('root'));

// JSX (shorter)
ReactDOM.render(<App />, document.getElementById('root'));

React.createElement()

Docs

// JS
React.createElement('button', {
  className: 'special-button',
  type: 'submit',
  disabled: true,
  children: 'Click me',
  onClick: () => alert('clicked'!)
})

// JSX
<button
  className="special-button"
  type="submit"
  disabled
  onClick={() => alert('clicked'!)}>Click me</button>

// JS
React.createElement(App, { env: 'development' })

// JSX
<App env="development" />

HTML Element props: i.e. style, className, role

Docs

// bad
<button class="special-button" style={{ 'margin-left': '20px' }} tab-index="0">click</button>

// good
<button className="special-button" style={{ marginLeft: '20px' }} tabIndex={0}>click</button>

// P.S. In `style`, you can pass number instead of `...px`:
<button style={{ marginLeft: 20 }}>click</button>

Functional Component

Docs

function Button(props: { label: string; onClick?(): void }) {
  return <button onClick={props.onClick}>{props.label}</button>;
}

// Usage
<Button label="Click me!" onClick={() => alert('clicked!')} />;

Using TS generic types and interfaces

Docs: TS functions

Docs: TS generic types

Docs: TS interfaces

interface ButtonProps {
  label: string;
  onClick?(): void;
}

function Button(props: ButtonProps) {
  return <button onClick={props.onClick}>{props.label}</button>;
}

Setting default prop values

Docs

interface ButtonProps {
  label: string;
  disabled: boolean;
  onClick?(): void;
}

function Button(props: ButtonProps) {
  return (
    <button disabled={props.disabled} onClick={props.onClick}>
      {props.label}
    </button>
  );
}

Button.defaultProps = {
  disabled: false
};

Exercise 1

  • Create Chat component that renders "I am a chat!"

  • Render <Chat /> in App

  • Create Row and Cell components with following props:

    interface RowProps {
      children?: React.ReactNode;
    }
    
    interface CellProps {
      header: boolean;
      padded: boolean;
      scrollable: boolean;
      textAlign: 'left' | 'center' | 'right';
      widthPercentage: number;
      height?: number;
      children?: React.ReactNode;
    }
    • Make Row render just a div.Row that renders the children in it

    • Make Cell render children in a div, that has following CSS class:

      • Cell - always
      • Cell--header - when props.header is true
      • Cell--padded - when props.padded is true
      • Cell--scrollable - when props.scrollable is true
      • Cell--textAlign-left - when props.textAlign === 'left' (and the same for other textAlign values)
    • In Cell, set the div's height to props.height px, and props.widthPercentage %

    • In Cell, provide default prop values for all the props besides height and children

  • In Chat, use the Row and Cell components to create chat layout, for example:

    • first row composed of two cells:

      • chat title
      • online users count
    • second row composed of two cells:

      • chat messages
      • online users list
    • third row composed of two cells:

      • textarea
      • login button

      Note: You don't have to create any components for the inside stuff of the chat! Instead, you can just print in cells something like "Here be chat title" ;)

  • (optionally) Create your own Button component

Lesson 2

Class Component

Docs: Converting a Function to a Class

// Simple component with no props
class Button extends React.Component {
  render() {
    return 'I am a button';
  }
}

// With props
interface ButtonProps {
  label: string;
  onClick?(): void;
}

class Button extends React.Component<ButtonProps> {
  render() {
    return <button onClick={props.onClick}>{props.label}</button>;
  }
}

State

Docs: Adding Local State to a Class

Implementing Accordion:

Accordion

interface AccordionProps {
  title: string;
  description: string;
}

interface AccordionState {
  extended: boolean;
}

class Accordion extends React.Component<AccordionProps, AccordionState> {
  state: AccordionState = {
    extended: false
  };

  render() {
    const { title, description } = this.props;
    const { extended } = this.state;

    return (
      <div className="Accordion">
        <div
          className="Accordion__title"
          onClick={() => this.setState({ extended: !extended })}
        >
          {title}
        </div>
        {extended && (
          <div className="Accordion__description">{description}</div>
        )}
      </div>
    );
  }
}
// Better
class Accordion extends React.Component<AccordionProps, AccordionState> {
  state: AccordionState = {
    extended: false
  };

  // Note: binding `this` is important here!
  handleTitleClick = () => {
    this.setState(state => {
      extended: !state.extended;
    });
  };

  render() {
    const { title, description } = this.props;
    const { extended } = this.state;

    return (
      <div className="Accordion">
        <div className="Accordion__title" onClick={this.handleTitleClick}>
          {title}
        </div>
        {extended && (
          <div className="Accordion__description">{description}</div>
        )}
      </div>
    );
  }
}

Component Lifecycle

Docs: Adding Lifecycle Methods to a Class

interface ClockState {
  date: Date;
}

class Clock extends React.Component<{}, ClockState> {
  state: ClockState = {
    date: new Date()
  };

  private timerID?: NodeJS.Timeout;

  componentDidMount() {
    this.timerID = setInterval(() => this.tick(), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.timerID!);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return <div>Current time: {this.state.date.toLocaleTimeString()}</div>;
  }
}

Element lists and keys

Docs

When rendering a list of elements, remember to add a unique key prop to all of them.

// Bad: Invalid JSX
return (
  <Component data={userA} />
  <Component data={userB} />
  <Component data={userC} />
);

// Bad: `key` is missing
return [
  <Component data={userA} />,
  <Component data={userB} />,
  <Component data={userC} />,
];

// Good
return [
  <Component key={userA.id} data={userA} />,
  <Component key={userB.id} data={userB} />,
  <Component key={userC.id} data={userC} />,
];

// Better
return [userA, userB, userC].map(user => (
  <Component key={user.id} data={user} />
));

P.S. When rendering a non-dynamic list, you can just use <React.Fragment> or <> instead (then key is not needed):

return (
  <>
    <Component data={userA} />
    <Component data={userB} />
    <Component data={userC} />
  </>
);

Exercise 2

Now we can add messages list.

  • Create MessagesList component with following props:
interface MessagesListProps {
  messages: Message[];
}
  • Use map function for mapping all messages. Try to make the html structure for the message similar to the one in the demo app. You can use div, strong and span tags.

  • To render the date you can use:

const date = new Date(message.createdAt);
date.toLocaleTimeString();
  • For this component you don't need any css classes, you can just add inline styles (color and font-weight) for username.

  • In Chat render MessagesList

Exercise 2b

It's time to add login and logout functionalities.

  • In Chat:

    • add currentUser: User | null to this.state

    • if currentUser exists then render a button that triggers this.handleSignOut when clicked

    • if currentUser doesn't exists then render a button that triggers this.handleSignIn when clicked

    • create this.handleSignIn method. Use prompt() to display a dialog boxes. You should ask an user for name and color. If the user gives the name, use login() from ChatAPI, otherwise show alert. When Promise is resolved set current user.

    • create this.handleSignOut method. Use logout() from ChatAPI and set current user to null.

Exercise 2c (optional)

  • Render users list.

Lesson 3

Controlled Components

Docs

class NameForm extends React.Component<
  { initialValue: string },
  { value: string }
> {
  constructor(props) {
    super(props);
    this.state = { value: '' };
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    this.setState({ value: event.target.value });
  }

  handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    alert('A name was submitted: ' + this.state.value);
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input
            type="text"
            value={this.state.value}
            onChange={this.handleChange}
          />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

textarea tag

Use value instead of children.

<textarea value={this.state.value} onChange={this.handleChange} />

Data must always flow down

Docs

// Bad
class Chat extends React.Component<
  {},
  {
    newMessageContent: string;
  }
> {
  state = {
    newMessageContent: ''
  };

  render() {
    return (
      <NewMessageForm
        content={this.state.newMessageContent}
        updateState={this.setState}
      />
    );
  }
}

// Good
class Chat extends React.Component<
  {},
  {
    newMessageContent: string;
  }
> {
  state = {
    newMessageContent: ''
  };

  handleContentChange = (nextContent: string) => {
    this.setState({ newMessageContent: nextContent });
  };

  render() {
    return (
      <NewMessageForm
        content={this.state.newMessageContent}
        onContentChange={this.handleContentChange}
      />
    );
  }
}

Never mutate props

// Bad
interface Props {
  message: Message;
  onChange?(message: Message): void;
}
interface State {
  inEdit: boolean;
}
class MessagePage extends React.Component<Props, State> {
  state: State = {
    inEdit: false
  };

  render() {
    const { message, onChange } = this.props;
    const { inEdit } = this.state;

    return (
      <>
        <MessagePreview message={message} />
        {inEdit && (
          <textarea
            value={message.content}
            onChange={event => {
              message.content = event.target.value;
              if (onChange) {
                onChange(message);
              }
            }}
          />
        )}
      </>
    );
  }
}

// Good
interface Props {
  message: Message;
  onChange?(message: Message): void;
}
interface State {
  inEdit: boolean;
}
class MessagePage extends React.Component<Props, State> {
  state: State = {
    inEdit: false
  };

  render() {
    const { message, onChange } = this.props;
    const { inEdit } = this.state;

    return (
      <>
        <MessagePreview message={message} />
        {inEdit && (
          <textarea
            value={message.content}
            onChange={event => {
              if (onChange) {
                onChange({ ...message, content: event.target.value });
              }
            }}
          />
        )}
      </>
    );
  }
}

// Good as well
interface Props {
  message: Message;
  onChange?(message: Message): void;
}
interface State {
  inEdit: boolean;
  editedMessage: Message;
}
class MessagePage extends React.Component<Props, State> {
  state: State = {
    inEdit: false,
    editedMessage: this.props.message
  };

  componentDidUpdate(prevProps: Props) {
    if (prevProps !== this.props.message) {
      this.setState({
        editedMessage: this.props.message
      });
    }
  }

  render() {
    const { onChange } = this.props;
    const { editedMessage, inEdit } = this.state;

    return (
      <form
        onSubmit={event => {
          event.preventDefault();
          onChange(editedMessage);
        }}
      >
        <MessagePreview message={editedMessage} />
        {inEdit && (
          <textarea
            value={editedMessage.content}
            onChange={event => {
              this.setState({
                editedMessage: { ...editedMessage, content: event.target.value }
              });
            }}
          />
        )}
      </form>
    );
  }
}

Exercise 3

Add a possibility to write and submit new messages:

  • Create MessageBox component, that:

    • has following props:

      interface MessageBoxProps {
        messageContent?: string;
        disabled?: boolean;
        onChange?: (value: string) => void;
        onSubmit?: () => void;
      }
    • renders <textarea>, that:

      • has value equal to props.messageContent
      • triggers props.onChange whenever its' value is changed
    • wraps <textarea> with <form>, that:

      • triggers props.onSubmit when form is submitted
    • renders a button, that triggers props.onSubmit when clicked

    • when <textarea> receives ENTER (but not SHIFT+ENTER), it triggers props.onSubmit

  • In Chat:

    • add newMessageContent: string to this.state

    • render MessageBox and control this.state.newMessageContent with it

    • once MessageBox#props.onSubmit is called, call ChatApi.createMessage()

Exercise 3b (optional)

  • Render nested thread messages (Message.submessages)

  • Add possibility to add a message to an existing thread (parentMessageId in ChatApi.createMessage(...))

Lesson 4

Tests with ReactTestUtils

Docs: Test Utilities

import React from 'react';
import ReactDOM from 'react-dom';
import { Button } from './Button';
import { Simulate } from 'react-dom/test-utils';

it('renders the button element', () => {
  const div = document.createElement('div');
  ReactDOM.render(<Button>text</Button>, div);

  const button = div.querySelector('button')!;
  expect(button).toBeTruthy();

  expect(button.textContent).toEqual('text');

  ReactDOM.unmountComponentAtNode(div);
});

it('calls onClick on click', () => {
  const onClick = jest.fn();
  const div = document.createElement('div');
  ReactDOM.render(<Button onClick={onClick} />, div);

  const button = div.querySelector('button')!;

  Simulate.click(button);
  expect(onClick).toHaveBeenCalledTimes(1);

  ReactDOM.unmountComponentAtNode(div);
});
//better
import React from 'react';
import ReactDOM from 'react-dom';
import { Button } from './Button';
import { Simulate } from 'react-dom/test-utils';

let div: HTMLDivElement;
beforeEach(() => {
  div = document.createElement('div');
});
afterEach(() => {
  ReactDOM.unmountComponentAtNode(div);
});

it('renders the button element', () => {
  ReactDOM.render(<Button>text</Button>, div);

  const button = div.querySelector('button')!;
  expect(button).toBeTruthy();
  expect(button.textContent).toEqual('text');
});

it('calls onClick on click', () => {
  const onClick = jest.fn();
  ReactDOM.render(<Button onClick={onClick} />, div);

  const button = div.querySelector('button')!;
  Simulate.click(button);
  expect(onClick).toHaveBeenCalledTimes(1);
});

Exercise 4

  • Create test for Cell. You only need to check if it renders correctly.
  • Create similar test for Row.

Exercise 4b (optional)

  • Create test for another component from outside ui directory.

Additional information

This project was bootstrapped with Create React App.

Code Splitting

This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting

Advanced Configuration

This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration

Branch Cheatsheet

git checkout lesson1 # Lesson 1
git checkout lesson2 # Lesson 2
git checkout lesson3 # Lesson 3
git checkout lesson4 # Lesson 4
git checkout master # After all lessons

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors