Hiển thị flash message khi chuyển trang trong ứng dụng React sử dụng react-router
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 ...
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, search và state như sau:
history.push({ pathname: '/posts', search: '?category=game' state: { redirectFrom: '/redirected_path' } });
Để 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>×</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