Hiển thị flash message khi chuyển trang trong ứng dụng React sử dụng react-router

Lý thuyết

Hiển thị flash message là một công việc có vẻ khá đơn giản. Chỉ cần một chút code như ví dụ sau là chúng ta đã có thể hoàn thành tính năng này:

import React, { useState } from 'react';
import { Alert, Button } from 'reactstrap';

export default ExampleComponent = () => {
  const [isFlashMessageShown, setIsFlashMessageShown] = useState(false);
  const showFlashMessage = () => setIsFlashMessageShown(true);
  const hideFlashMessage = () => setIsFlashMessageShown(false);

  return (
    <div>
      <Alert
        color='primary'
        isOpen={isFlashMessageShown}
        toggle={hideFlashMessage}
      >
        Hello there!
      </Alert>
      <Button color='primary' onClick={showFlashMessage}>
        Show flash message
      </Button>
    </div>
  );
};

Với ví dụ trên, flash message sẽ được hiển thị bên trong ExampleComponent. Tuy nhiên, nó chỉ được hiển thị bên trong ExampleComponent với code điều khiển ẩn/hiện trong component này mà thôi. Nếu chúng ta muốn flash message được hiển thị khi redirect từ một component khác (sử dụng react-router), chẳng hạn như khi tạo dữ liệu thành công ở form nhập liệu, chúng ta redirect về trang hiển thị danh sách dữ liệu và hiển thị flash message ở trang danh sách này thì sao?

Với ứng dụng React dùng react-router, chúng ta dùng

  history.push(path)

để thực hiện việc redirect đến path được định nghĩa ở các component Route(*). Ngoài tham số path ở trên, hàm push còn có thể nhận 1 tham số nữa là state, đây là object chứa dữ liệu về trạng thái của location. Khi gọi hàm push, location mới chứa pathname, search, hash (được phân tích từ path) và state sẽ được đẩy vào history stack của react-router. Ở component được redirect đến, location của component này chính là location mới vừa được đẩy vào history stack đó.

Như vậy, chúng ta có thể lợi dụng việc này để gán flash message vào state, khi hiển thị component được redirect đến thì lấy dữ liệu flash message ở state của location ra và hiển thị.

(*) hàm history.push còn có thể nhận một tham số là object chứa các dữ liệu về pathname, searchstate như sau:

history.push({
  pathname: '/posts',
  search: '?category=game'
  state: { redirectFrom: '/redirected_path' }
});

Ví dụ

Để minh họa, chúng ta sẽ làm một ứng dụng React ví dụ có các chức năng tạo và hiển thị danh sách ghi chú. Khi tạo ghi chú thành công, ứng dụng sẽ redirect về trang danh sách ghi chú và hiển thị flash message thông báo tạo ghi chú thành công ở trên trang danh sách này.

Thêm + hiển thị danh sách ghi chú

Tạo project mới bằng create-react-app, sau đó di chuyển vào thư mục này:

create-react-app flash-message
cd flash-message

Thêm css của bootstrap vào file public/index.html:

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

Thêm package react-router-dom:

yarn add react-router-dom

Tạo file src/NoteForm.jsx chứa form tạo ghi chú. Form gồm một textarea dùng để nhập nội dung ghi chú và một nút submit. Khi submit form, hàm addNote truyền từ props sẽ được dùng để tạo ghi chú, tạo xong sẽ redirect về /.

import React, { useState } from 'react';
import { withRouter } from 'react-router-dom';

const NoteForm = ({ history, addNote }) => {
  const [content, setContent] = useState('');
  const submitForm = event => {
    event.preventDefault();
    addNote(content);
    history.push('/');
  };

  return (
    <div className='row mt-5'>
      <div className='col-md-10 offset-md-1'>
        <h4>Add Note</h4>
        <form onSubmit={submitForm}>
          <div className='form-group'>
            <textarea
              id='content'
              className='form-control'
              rows='10'
              placeholder='Enter content'
              onChange={e => setContent(e.target.value)}
            />
          </div>
          <div className='form-group'>
            <button type='submit' className='btn btn-primary'>Submit</button>
          </div>
        </form>
      </div>
    </div>
  );
};

export default withRouter(NoteForm);

Tạo file src/Notes.jsx chứa component hiển thị danh sách ghi chú lấy từ notes của props. Ghi chú gồm nội dung và thời gian tạo.

import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';

const Notes = ({ notes }) => {
  return (
    <Fragment>
      <div className='mt-5'>
        <div className='row'>
          <div className='col-md-10 offset-md-1'>
            <h4>Notes</h4>
            <div className='text-center'>
              <Link className='btn btn-primary' to='/create_note'>
                Create note
              </Link>
            </div>
          </div>
        </div>
        <div className='row mt-4'>
          <div className='col-md-10 offset-md-1'>
            <ul className='notes'>
              {
                notes.map((note, index) => (
                  <li className='note' key={index}>
                    <p className='note-content'>{note.content}</p>
                    <p className='note-createdAt'>{note.createdAt}</p>
                  </li>
                ))
              }
            </ul>
          </div>
        </div>
      </div>
    </Fragment>
  );
};

export default Notes;

Sửa lại file src/App.js. App mới sẽ có

  • state notes chứa các ghi chú đã được tạo. notes sẽ được truyền vào component Notes.
  • hàm addNote dùng để thêm ghi chú mới vào notes. Hàm này sẽ được truyền vào component NoteForm.
  • các route khai báo đường link và component tương ứng. / ứng với component Notes, /create_note ứng với NoteForm.
  • các hàm tiện ích để tạo giá trị createdAt cho ghi chú khi tạo ghi chú.
import React, { useState } from 'react';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
} from 'react-router-dom';

import Notes from './Notes';
import NoteForm from './NoteForm';

const zeroPadded = num => num < 10 ? `0${num}` : num;

const timestamp = () => {
  const time = new Date();
  const year = time.getFullYear();
  const month = zeroPadded(time.getMonth() + 1);
  const day = zeroPadded(time.getDate());
  const hour = zeroPadded(time.getHours());
  const minute = zeroPadded(time.getMinutes());
  const second = zeroPadded(time.getSeconds());
  return `${year}/${month}/${day} ${hour}:${minute}:${second}`;
};

const App = () => {
  const [notes, setNotes] = useState([]);

  const addNote = content => {
    const newNote = {
      content,
      createdAt: timestamp()
    }

    setNotes([newNote, ...notes]);
  };

  return (
    <Router>
      <div className='container'>
        <header className='header'>
          <ul className='nav'>
            <li className='nav-item'>
              <Link className='nav-link active' to='/'>Notes</Link>
            </li>
          </ul>
        </header>
        <Switch>
          <Route exact path='/create_note'>
            <NoteForm addNote={addNote}/>
          </Route>
          <Route exact path='/'>
            <Notes notes={notes}/>
          </Route>
        </Switch>
      </div>
    </Router>
  );
};

export default App;

Thêm css vào file src/index.css:

.header {
  margin-top: 10px;
  margin-bottom: 10px;
  border-radius: 5px;
  border: 1px solid gray;
}

.notes {
  list-style: none;
  padding-left: 0px;
}

.note {
  border: 1px solid rgba(0, 0, 0, 0.2);
  border-radius: 5px;
  padding: 10px;
  margin-bottom: 10px;
}

.note-content {
  white-space: pre-wrap;
}

.note-createdAt {
  font-size: 0.7em;
  margin-bottom: 0px;
  text-align: right;
}

Chạy ứng dụng bằng lệnh yarn start. Chúng ta có thể tạo ghi chú, tuy nhiên chưa có flash message khi redirect về trang danh sách ghi chú. Chúng ta sẽ thêm flash message ở bước sau.

Thêm flash message

Tạo file src/FlashMessage.jsx chứa component FlashMessage:

import React from 'react';

const FlashMessage = ({ flashMessage, close }) => {
  const { type, message } = flashMessage;

  if (message) {
    return (
      <div className={`alert alert-${type}`}>
        <button type='button' className='close' onClick={close}>
          <span>&times;</span>
        </button>
        {message}
      </div>
    );
  }

  return null;
};

export default FlashMessage;

Tại file src/Notes.jsx, lấy flashMessage trong state của location ra và hiển thị:

// import thêm useState
import React, { Fragment, useState } from 'react';
// import thêm withRouter
import { withRouter, Link } from 'react-router-dom';

// import thêm FlashMessage
import FlashMessage from './FlashMessage';

// lấy thêm location từ props
const Notes = ({ location, notes }) => {
  // lấy flashMessage ra từ state của location
  // nếu không có thì gán bằng object rỗng
  const { flashMessage: fm } = location.state || {};
  const redirectedFlashMessage = fm || {};

  const [flashMessage, setFlashMessage] = useState(redirectedFlashMessage);

  return (
    <Fragment>
      {/* hiển thị flash message */}
      <FlashMessage
        flashMessage={flashMessage}
        close={() => setFlashMessage({})}
      />
      ...
    </Fragment>
  );
};

// gói Notes trong higher order component withRouter
export default withRouter(Notes);

Tại hàm submitForm của NoteForm, truyền thêm state khi redirect về /:

const submitForm = event => {
  event.preventDefault();
  addNote(content);
  history.push('/', {
    flashMessage: { type: 'success', message: 'Create note successfully!' }
  });
};

Lúc này, khi tạo note xong và redirect về / thì flash message với nội dung Create note successfully! sẽ xuất hiện.

Sửa lỗi khi nhấn nút Back trên trình duyệt

Sau khi tạo xong ghi chú và redirect về /, chuyển sang form tạo ghi chú thì flash message sẽ biến mất. Nhưng nếu tiếp đó bạn nhấn vào nút Back trên trình duyệt thì trang danh sách ghi chú sẽ hiện lại và flash message lại hiện ra tiếp.

Nguyên nhân của việc này là do react-router lấy location cũ từ stack ra và location này vẫn chứa flashMessage trong state của nó. Component Notes sẽ chạy lại đoạn code lấy flashMessage từ state của location và hiển thị lại flashMessage này.

Để flash message không hiển thị lại như vậy, chúng ta sẽ xóa luôn flashMessage trong state của location khi lấy nó ra. Sửa lại src/Notes.jsx như sau:

// import thêm history
const Notes = ({ history, location, notes }) => {
  let redirectedFlashMessage = {};
  const { pathname, search, state } = location;

  if (state && state.flashMessage) {
    redirectedFlashMessage = state.flashMessage;

    // copy state cũ
    const clonedState = { ...state };
    // xóa flashMessage từ state được copy
    delete clonedState.flashMessage;
    // thay thế location hiện tại bằng location mới
    // với pathname, search từ location hiện tại
    // và state đã xóa flashMessage
    history.replace({ pathname, search, state: clonedState });
  }

  ...
};

Thử lại trường hợp nhấn nút Back ở trên, flash message sẽ không hiện lại nữa.

Chuyển code xử lý flash message vào hook

Để tiện cho việc tái sử dụng, chúng ta có thể chuyển đoạn code xử lý flash message trong component Notes vào hook useFlashMessage. Các component khác có thể sử dụng hook này để lấy flashMessage từ location và hiển thị.

Tạo file src/use_flash_message.jsx với nội dung như sau:

import React, { useState } from 'react';

import FlashMessage from './FlashMessage';

const useFlashMessage = (location, history) => {
  let redirectedFlashMessage = {};
  const { pathname, search, state } = location;
  if (state && state.flashMessage) {
    redirectedFlashMessage = state.flashMessage;

    const clonedState = { ...state };
    delete clonedState.flashMessage;
    history.replace({ pathname, search, state: clonedState });
  }

  const [flashMessage, setFlashMessage] = useState(redirectedFlashMessage);
  const flashMessageComponent = () => (
    <FlashMessage
      flashMessage={flashMessage}
      close={() => setFlashMessage({})}
    />
  );

  return flashMessageComponent;
};

export default useFlashMessage;

Hook useFlashMessage sẽ trả về hàm flashMessageComponent, chúng ta có thể dùng hàm này như một component để hiển thị flash message. Sửa lại src/Notes.jsx:

import React, { Fragment, useState } from 'react';
import { withRouter, Link } from 'react-router-dom';

// bỏ import FlashMessage
// import useFlashMessage
import useFlashMessage from './use_flash_message';

const Notes = ({ history, location, notes }) => {
  const FlashMessage = useFlashMessage(location, history);

  return (
    <Fragment>
      {/* sửa lại code hiển thị flash message */}
      <FlashMessage />
      ...
    </Fragment>
  );
};

export default withRouter(Notes);

Mở rộng: chúng ta có thể trả về thêm flashMessage, setFlashMessage từ useFlashMessage để tùy ý sử dụng, ví dụ như dùng setFlashMessage để hiển thị flashMessage với nội dung mới ngay tại trang hiện tại.

Kết luận

Qua lý thuyết và ví dụ được trình bày ở trên, hi vọng các bạn đang gặp vướng mắc khi hiển thị flash message lúc chuyển trang có thể tham khảo vào giải quyết vấn đề một cách suôn sẻ. Cảm ơn các bạn đã theo dõi bài viết.