はじめに
ReactとMaterial-UI(MUI)を使ってちょっとしたwebアプリを作りたいと思い、現在勉強中の身です。
課題
React+Material-UIを使う事でいわゆるタブ機能を組み込む事が出来ます。(こちらの記事のコードを参考にしています。)
const Left = () => {
const [value, setValue] = React.useState(0);
const handleCount = () => {
setValue(prev => prev + 1);
}
return (
<Grid container>
<Grid item xs={3}><Item>{value}</Item></Grid>
<Grid item xs={6}><Item><Button variant="contained" onClick={() => { handleCount }}>Count</Button></Item></Grid>
<Grid item xs={3}><Item>Left</Item></Grid>
</Grid>
);
}
const Right = () => {
const [value, setValue] = React.useState(0);
const handleCount = () => {
setValue(prev => prev + 1);
}
return (
<Grid container>
<Grid item xs={3}><Item>{value}</Item></Grid>
<Grid item xs={6}><Item><Button variant="contained" onClick={() => { handleCount }}>Count</Button></Item></Grid>
<Grid item xs={3}><Item>Right</Item></Grid>
</Grid>
);
}
const App = () => {
return (
<CenteredTabs labels={['left', 'right']}>
<div><Left /></div>
<div><Right /></div>
</CenteredTabs>
);
}
しかしこの実装の場合、
タブを切り替える度に、コンポーネントが持っているstateの値が初期化されてしまう問題が生じます。
- 例えばカウントした数字がタブ切り替え時に初期化されて0に戻ってしまいます
解決方法
まず原因は子コンポーネント側だけでstateを管理しようとしているためであると考えられます。
つまり、親側でstateを管理させれば良い訳です。
そのために以下の2つを実装する必要があります。
-
親コンポーネントにstateを管理させて、子コンポーネントで値を更新した際に親側からデータを送る
-
親コンポーネントが持っているstateを子に送る
ここではAppが親コンポーネント、Left/Rightが子コンポーネントに当たります。
以下、実装はこちらのサイトを参考にしています。
親から子へのデータの渡し方
親コンポーネントから子コンポーネントへのデータの渡し方は簡単で、propsを使って渡します。
- 親側
const App = () => {
const [left_value, setleftValue] = React.useState(0);
const [right_value, setrightValue] = React.useState(0);
return (
<CenteredTabs labels={['left', 'right']}>
<div><Left value={left_value} /></div>
<div><Right value={right_value} /></div>
</CenteredTabs>
);
}
LeftとRightどちらのコンポーネントもカウンタ機能を有しているので、stateは2つ用意しておきます。
上ではLeftにleft_valueを、Rightにright_valueを渡しています。
- 子側
const Left = (props) => {
const [value, setValue] = React.useState(props.value);
const handleCount = () => {
setValue(prev => prev + 1);
}
return (
<Grid container>
<Grid item xs={3}><Item>{value}</Item></Grid>
<Grid item xs={6}><Item><Button variant="contained" onClick={() => { handleCount}}>Count</Button></Item></Grid>
<Grid item xs={3}><Item>Left</Item></Grid>
</Grid>
);
}
const Right = (props) => {
const [value, setValue] = React.useState(props.value);
const handleCount = () => {
setValue(prev => prev + 1);
}
return (
<Grid container>
<Grid item xs={3}><Item>{value}</Item></Grid>
<Grid item xs={6}><Item><Button variant="contained" onClick={() => {handleCount}}>Count</Button></Item></Grid>
<Grid item xs={3}><Item>Right</Item></Grid>
</Grid>
);
}
コンポーネント側ではprops.valueで、親から送られた値を受け取っています。
子から親へのデータの渡し方
こちら側は少し複雑な処理が必要です。
- 親側
まず親側に、子からデータを受け取る関数を用意します。
const App = () => {
const [left_value, setleftValue] = React.useState(0);
const [right_value, setrightValue] = React.useState(0);
const childToParentL = (child_data) => {
setleftValue(child_data);
}
const childToParentR = (child_data) => {
setrightValue(child_data);
}
return (
<CenteredTabs labels={['left', 'right']}>
<div><Left value={left_value} childToParent={childToParentL}/></div>
<div><Right value={right_value} childToParent={childToParentR}/></div>
</CenteredTabs>
);
}
引数のchild_dataが子から送られてくるデータになります。
この関数ではchild_dataを親が持つstateの値としてセットします。
そして関数をpropsとして子コンポーネント側に渡します。
- 子側
子コンポーネント側では受け取った関数を子側で呼び出す事により親へデータを送る事が出来ます。
const Left = (props) => {
const [value, setValue] = React.useState(props.value);
let data = value;
const handleCount = () => {
data = data + 1;
setValue(data);
}
return (
<Grid container>
<Grid item xs={3}><Item>{value}</Item></Grid>
<Grid item xs={6}><Item><Button variant="contained" onClick={() => { handleCount(); props.childToParent(data);}}>Count</Button></Item></Grid>
<Grid item xs={3}><Item>Left</Item></Grid>
</Grid>
);
}
Leftコンポーネントを例に挙げます。
ここでは新しくdataという変数を宣言しています。
このdataの値をchildToParentで親へ渡す値としています。
カウントボタンを押す事でhandleCount関数(カウント関数)および親から渡されたchildToParent関数を呼び出します。
ここでvalueを直接親へ渡していないのは、valueを渡すようにした場合、更新を行う直前の値が親側へ渡されるという現象が見られたためです。
例えばカウンタの値が15となっている時に、一旦タブを切り替えて戻ると、カウンタが14に戻ってしまいます。
最終的に以下のようなコードになります。
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Paper from '@mui/material/Paper';
import Button from '@mui/material/Button';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
const Item = styled(Paper)(({ theme }) => ({
textAlign: 'center',
padding: '20px',
margin: '20px',
fontSize: '20px',
}));
const useStyles = styled({
root: {
flexGrow: 1,
},
});
const TabPanel = (props) => {
const { children, value, index, change, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={'simple-tabpanel-${index}'}
aria-labelledby={'simple-tab-${index}'}
{...other}>
{value === index && (
<Box p={3}>
{children}
</Box>
)}
</div>
);
}
const CenteredTabs = (props) => {
const classes = useStyles();
const [value, setValue] = React.useState(0);
const handleChangeTabs = (event, newValue) => {
setValue(newValue);
};
return (
<div>
<Paper className={classes.root}>
<Tabs
value={value}
onChange={handleChangeTabs}
indicatorColor="primary"
textColor="primary"
centered
>
{props.labels.map(label => <Tab label={label}></Tab>)}
</Tabs>
</Paper>
{props.children.map((child, index) =>
<TabPanel value={value} index={index}>{child}</TabPanel>)
}
</div>
);
}
const Left = (props) => {
const [value, setValue] = React.useState(props.value);
let data = value;
const handleCount = () => {
data = data + 1;
setValue(data);
}
return (
<Grid container>
<Grid item xs={3}><Item>{value}</Item></Grid>
<Grid item xs={6}><Item><Button variant="contained" onClick={() => { handleCount(); props.childToParent(data);}}>Count</Button></Item></Grid>
<Grid item xs={3}><Item>Left</Item></Grid>
</Grid>
);
}
const Right = (props) => {
const [value, setValue] = React.useState(props.value);
let data = value;
const handleCount = () => {
data = data + 1;
setValue(data);
}
return (
<Grid container>
<Grid item xs={3}><Item>{value}</Item></Grid>
<Grid item xs={6}><Item><Button variant="contained" onClick={() => {handleCount(); props.childToParent(data);}}>Count</Button></Item></Grid>
<Grid item xs={3}><Item>Right</Item></Grid>
</Grid>
);
}
const App = () => {
const [left_value, setleftValue] = React.useState(0);
const [right_value, setrightValue] = React.useState(0);
const childToParentL = (child_data) => {
setleftValue(child_data);
}
const childToParentR = (child_data) => {
setrightValue(child_data);
}
return (
<CenteredTabs labels={['left', 'right']}>
<div><Left value={left_value} childToParent={childToParentL}/></div>
<div><Right value={right_value} childToParent={childToParentR}/></div>
</CenteredTabs>
);
}
export default App;
切り替えてもカウントした値が初期化される事無くちゃんと残ったままになっています。
参考文献