您可以通过左右滑屏查看更多笔记内容。。

React状态管理与进阶

08 Sep 2019 . category: tech .
更多笔记

React状态管理与进阶

一.状态提取

状态提取:将子状态(substate)从一个组件移动到其他组件中的重构过程被称为状态提取。

状态提升:状态提取的过程也可以反过来,从子组件到父组件,这种情形被称为状态提升。

案例:

将状态和有关排序的方法从 App 组件向下移动到 Table 组件中。

import React, { Component } from 'react';
// import logo from './logo.svg';
import './App.css';
import {sortBy} from 'lodash';
// 设置好 URL 常量和默认参数,将 API 请求分解成几步
const DEFAULT_QUERY = 'redux';
// 每次抓取的资讯数量
const DEFAULT_HPP='10';
const PATH_BASE = 'https://hn.algolia.com/api/v1';
const PATH_SEARCH = '/search';
const PARAM_SEARCH = 'query=';
const PARAM_PAGE = 'page=';
const PARAM_HPP='hitsPerPage=';
const Loading =()=><div>Loading...</div>
// 定义一个排序函数,用于返回未排序的列表
const SORTS={
  NONE:list=>list,
  TITLE: list=>sortBy(list,'title'),
  AUTHOR: list=>sortBy(list,"author"),
  COMMENTS: list=>sortBy(list,'num_comments').reverse(),
  POINTS:list=>sortBy(list,'points').reverse(),
};
// function withFoo(Component){
//   return function(props){
//     return <Component{...props}/>
//   }
// }
// const withFoo=(Component)=>(props)=><Component{...props}/>
//当加载状态 (isLoading) 为 true 时,组件显示 Loading 组件,否则显示输入的组件。
const withLoading =(Component)=>({isLoading,...rest})=>
  isLoading?
  <Loading/>
  :<Component{...rest}/>

// 使用模板字符串(template strings)进行链接字符
// const url = `${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTeam}&${PARAM_PAGE}`;
// console.log(url);
// 使用真实的API
class App extends Component{

  constructor(props){
    super(props);
    this.state={
      // 用来存储每次能够查询到的内容
      results:null,
      searchKey:'',
      searchTeam:DEFAULT_QUERY,
      // 存储错误信息的‘
      error:null,
      //存储加载状态
      isLoading:false,
    }
    this.setSearchTopStories = this.setSearchTopStories.bind(this);
    this.fetchSearchTopStories = this.fetchSearchTopStories.bind(this);
    this.onSerachChange = this.onSerachChange.bind(this);
    this.onDissmisss = this.onDissmisss.bind(this);
    this.onSerachSubmit=this.onSerachSubmit.bind(this);
  }
  // 如果搜索的结果已经存在于缓存中,就阻止 API 请求。
  needsToSearchTopStories(searchTeam){
    return !this.state.results[searchTeam];
  }
  onSerachSubmit(event){
    const {searchTeam}=this.state;
    this.setState({searchKey:searchTeam})

    // this.fetchSearchTopStories(searchTeam);
    
    if(this.needsToSearchTopStories(searchTeam)){
      this.fetchSearchTopStories(searchTeam);
    }
    event.preventDefault();
  }
  //删除函数
  onDissmisss(id){
    const {searchKey,results}=this.state;
    const {hits,page}=results[searchKey];
    // 找到除点击外的其他id
    const isNotId=item=>item.objectID!==id;
    //在原数组中筛选包含其他id的一个新的数组
    const updatedHits=hits.filter(isNotId);
    console.log(results);
    // 重新设置state
    this.setState({
      results: {
        ...results,
        [searchKey]: { hits: updatedHits, page }
      }
    });
  }
  //onChange函数,event对象的target属性中带有输入框的值
  onSerachChange(event){
    // 得到我输入框的内容
    this.setState({searchTeam:event.target.value});
  }
  setSearchTopStories(result) {
    // 获取result 中的hits 与 page
    const {hits,page}=result;
    // 判断老的hits字段是否存在,当页码为0时,hits 是空的
    //当点击more 时,页码不为0,此时他是下一页,老的 hits 已经存在状态中等待着与新的分页合并。
    const {searchKey,results}=this.state;
    // 从组件状态中检索 searchKey,中
    const oldHits=results&&results[searchKey]  
    ? results[searchKey].hits:[];
    // 合并老的hits.及API返回的新的hits, 
    const updatedHits=[
      ...oldHits,
      ...hits
    ]
    //将合并后的 hits 和页码设置到本地组件的状态中。一个新的 result 可以设置在 results 集中
    this.setState({ 
      results: {
        ...results,
        [searchKey]: { hits: updatedHits,page}
      },
      isLoading:false     
     });
  }
  fetchSearchTopStories(searchTeam,page=0) {
    this.setState({isLoading:true});
    fetch(`${PATH_BASE}${PATH_SEARCH}?${PARAM_SEARCH}${searchTeam}&${PARAM_PAGE}${page}&${PARAM_HPP}${DEFAULT_HPP}`)
   //返回的响应需要被转化成 JSON 格式的数据结构
    .then(response => response.json())
    .then(result => this.setSearchTopStories(result))
    //如果在发起请求时出现错误,这个函数会进入到 catch 中而不是 then 中
    .catch(e => this.setState({error:e}));
  }
  // 获取数据
  componentDidMount() {
    const { searchTeam } = this.state;
    this.setState({searchKey:searchTeam})
    this.fetchSearchTopStories(searchTeam);
  }

  render(){
      const { searchTeam, results,searchKey,error,isLoading} = this.state;
      const page=(results && results[searchKey]&&results[searchKey].page) || 0;    
      const list=(results && results[searchKey]&&results[searchKey].hits) || []; 
      // 条件渲染
      // if (!result) { return null; }
      // 有错误发生时,获取error,利用条件渲染来显示一个错误信息。
      if(error){
        return <p>Something went wrong</p>
      }
        return (
          <div className="page">
             <div className="interactions">          
              {/* 用于搜索的输入组件 */}
                <Search
                  value={searchTeam}
                  onChange={this.onSerachChange}
                  onSubmit={this.onSerachSubmit}
                  >
                    Search
                  </Search>
            </div>
            {error?
            <div className="interactions">
              <p>Something went wrong</p>
              </div>
              :
              <Table
              list={list}
              onDissmisss={this.onDissmisss}
              />
          } 
           <div className="interactions">
             <ButtonWithLoading
              isLoading={isLoading}
                onClick={() => this.fetchSearchTopStories(searchKey, page + 1)}>
                More
            </ButtonWithLoading>
            </div>   
          </div>    
    );
  }
}
const Button=({onClick,className='',children,})=>
    <button 
      onClick={onClick}
      className={className}
      type="button"
    >
      {children}
    </button>
const ButtonWithLoading=withLoading(Button);
//Search 组件
class Search extends Component{
  // 使用 this 对象、适当的生命周期方法和 DOM API 在组件挂载的时候来聚焦 input 字段。
  componentDidMount(){
    if(this.input){
      this.input.focus();
    }
  }
  render(){
    const {
      value,onChange,onSubmit,children}=this.props;
  return(
    <form onSubmit={onSubmit}>
      <input
        type="text"
        value={value}
        onChange={onChange}
        ref={(node)=>{this.input=node;}}
      />
      <button type="submit">
        {children}
      </button>
    </form>
  )
}
}
const Sort=({sortKey,activeSortKey,onSort,children})=>{
  const sortClass=['button-inline'];
  if(sortKey===activeSortKey){
    sortClass.push('button-active');
  }
  return(
  <Button onClick={()=>onSort(sortKey)} className={sortClass.join('')}>
    {children}
  </Button>
  );}
  //列表样式
  const largeColumn = {
    width: '40%',
    };
    const midColumn = {
    width: '30%',
    };
    const smallColumn = {
    width: '10%',
    };  
class Table extends Component{
  constructor(props){
    super(props);
    // 将状态和有关排序的方法从 App 组件向下移动到 Table 组件中。
    this.state={
        sortKey:'NONE',
        isSortReverse:false,
    };
    this.onSort=this.onSort.bind(this);
  }
  onSort(sortKey){
    const isSortReverse=this.state.sortKey===sortKey && !this.state.isSortReverse;
    this.setState({sortKey,isSortReverse})
  }
    render(){
      const {list,onDissmisss}=this.props;
      const {sortKey,isSortReverse}=this.state;
      const sortedList=SORTS[sortKey](list);
      const reverseSortedList=isSortReverse?sortedList.reverse():sortedList;
      return(
        <div className="table">
          <div className="table-header">
              <span style=>
                <Sort
                  sortKey={'TITLE'}
                  onSort={this.onSort}
                  activeSortKey={sortKey}
                  >
                  Title
                </Sort>
              </span>
              <span style=>
                <Sort
                  sortKey={'AUTHOR'}
                  onSort={this.onSort}
                  activeSortKey={sortKey}
                  >
                  Author
                </Sort>
              </span>
              <span style=>
                <Sort
                  sortKey={'COMMENTS'}
                  onSort={this.onSort}
                  activeSortKey={sortKey}
                  >
                  Comments
                </Sort>
              </span>
              <span style=>
                <Sort
                  sortKey={'POINTS'}
                  onSort={this.onSort}
                  activeSortKey={sortKey}
                  >
                  Points
                </Sort>
              </span>
              <span style=>
                Archive
              </span>
              </div>
          {reverseSortedList.map(item=>
            <div key={item.objectID} className="table-row">
                <span style={midColumn}>
                  <a href={item.url}>{item.title}</a>
                </span>
                <span style={smallColumn}>{item.points}</span>
                <span style={largeColumn}>{item.author}</span>
                <span style={midColumn}>{item.num_comments}</span>
                <span style={midColumn}>
                  <Button onClick={()=>onDissmisss(item.objectID)}
                          className="button-inline"
                  >
                    Dissmiss  
                  </Button> 
                </span>
            </div>
            )}
        </div>
      );
    }
}
export default App;