[React] Refactor a Class Component with React hooks to a Function
We have a render prop based class component that allows us to make a GraphQL request with a given query string and variables and uses a GitHub graphql client that is in React context to make the request. Let's refactor this to a function component that uses the hooks useReducer, useContext, and useEffect.
Class Based Component:
import {Component} from 'react' import PropTypes from 'prop-types' import isEqual from 'lodash/isEqual' import * as GitHub from '../../../github-client' class Query extends Component { static propTypes = { query: PropTypes.string.isRequired, variables: PropTypes.object, children: PropTypes.func.isRequired, normalize: PropTypes.func, } static defaultProps = { normalize: data => data, } static contextType = GitHub.Context state = {loaded: false, fetching: false, data: null, error: null} componentDidMount() { this._isMounted = true this.query() } componentDidUpdate(prevProps) { if ( !isEqual(this.props.query, prevProps.query) || !isEqual(this.props.variables, prevProps.variables) ) { this.query() } } componentWillUnmount() { this._isMounted = false } query() { this.setState({fetching: true}) const client = this.context client .request(this.props.query, this.props.variables) .then(res => this.safeSetState({ data: this.props.normalize(res), error: null, loaded: true, fetching: false, }), ) .catch(error => this.safeSetState({ error, data: null, loaded: false, fetching: false, }), ) } safeSetState(...args) { this._isMounted && this.setState(...args) } render() { return this.props.children(this.state) } } export default Query
Conver props:
// From static propTypes = { query: PropTypes.string.isRequired, variables: PropTypes.object, children: PropTypes.func.isRequired, normalize: PropTypes.func, } static defaultProps = { normalize: data => data, } // To: function Query ({query, variables, children, normalize = data => data}) { }
Conver Context:
// From static contextType = GitHub.Context ... const client = this.context // To: import {useContext} from 'react' function Query ({query, variables, children, normalize = data => data}) { const clinet = useContext(GitHub.Context) }
Conver State:
I don't like to cover each state prop to 'useState' style, it is lots of DRY, instead, using useReducer is a better & clean apporach.
// From state = {loaded: false, fetching: false, data: null, error: null} //To: import {useContext, useReducer} from 'react' ... const [state, setState] = useReducer( (state, newState) => ({...state, ...newState}), defaultState)
Conver side effect:
// From: componentDidMount() { this._isMounted = true this.query() } componentDidUpdate(prevProps) { if ( !isEqual(this.props.query, prevProps.query) || !isEqual(this.props.variables, prevProps.variables) ) { this.query() } } componentWillUnmount() { this._isMounted = false } query() { this.setState({fetching: true}) const client = this.context client .request(this.props.query, this.props.variables) .then(res => this.safeSetState({ data: this.props.normalize(res), error: null, loaded: true, fetching: false, }), ) .catch(error => this.safeSetState({ error, data: null, loaded: false, fetching: false, }), ) } // To: useEffect(() => { setState({fetching: true}) client .request(query, variables) .then(res => setState({ data: normalize(res), error: null, loaded: true, fetching: false, }), ) .catch(error => setState({ error, data: null, loaded: false, fetching: false, }), ) }, [query, variables]) // trigger the effects when 'query' or 'variables' changes
Conver render:
// From: render() { return this.props.children(this.state) } // To: function Query({children ... }) { ... return children(state); }
-----
Full Code:
import {useContext, useReducer, useEffect} from 'react' import PropTypes from 'prop-types' import isEqual from 'lodash/isEqual' import * as GitHub from '../../../github-client' function Query ({query, variables, children, normalize = data => data}) { const client = useContext(GitHub.Context) const defaultState = {loaded: false, fetching: false, data: null, error: null} const [state, setState] = useReducer( (state, newState) => ({...state, ...newState}), defaultState) useEffect(() => { setState({fetching: true}) client .request(query, variables) .then(res => setState({ data: normalize(res), error: null, loaded: true, fetching: false, }), ) .catch(error => setState({ error, data: null, loaded: false, fetching: false, }), ) }, [query, variables]) // trigger the effects when 'query' or 'variables' changes return children(state) } export default Query
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2017-02-17 [ES2016] Check if an array contains an item using Array.prototype.includes
2017-02-17 [Ramda] Handle Errors in Ramda Pipelines with tryCatch
2016-02-17 [Regular Expressions] Match the Start and End of a Line
2016-02-17 [ES7] Object.observe + Microtasks