Skip to main content

Redux Toolkit Github API

TST, Hongkong

Scaffolding

npm create vite@latest redux-toolkit
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC
cd redux-toolkit
npm install
npm install @mui/material @emotion/react @emotion/styled @fontsource/roboto
npm install @reduxjs/toolkit react-redux @types/react-redux @types/webpack-env

Use baseURL to be able to work with absolute imports relative to the defined base:

./tsconfig.json

{
"compilerOptions": {
"baseUrl": "./src",
...

./vite.config.js npm install vite-tsconfig-paths

import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import react from '@vitejs/plugin-react-swc'

export default defineConfig({
plugins: [
react(),
tsconfigPaths()
],
})

_./src/pages/App.tsx

import Box from '@mui/material/Box';
import HelloWorld from 'components/HelloWorld'

import 'styles/App.css'

export default function App() {

return (
<>
<Box sx={{ width: '100%', maxWidth: 1000 }}>
<HelloWorld greeting='Hello from React Typescript' />
</Box>
</>
)
}

_./src/components/HelloWorld.tsx

import React from 'react'
import Typography from '@mui/material/Typography';

import { iHelloProps } from 'types/interfaces'

export default function HelloWorld({ greeting }: iHelloProps): React.JSX.Element {
return (
<Typography variant="h1" gutterBottom>
{ greeting }
</Typography>
)
}
npm run dev

Project Tracker

Layout

./src/components/ProjectCard.tsx

import React from "react"
import { Typography, Grid, Stack, Paper} from "@mui/material"

interface IProps {
issueTitle: string
}

export default function ProjectCard({ issueTitle }: IProps): React.JSX.Element {
return(
<div className="project_card">
<Paper elevation={1} sx={{p: '10px', m:'1rem'}}>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Stack spacing={2}>
<Typography variant="h6" sx={{fontWeight: 'bold'}}>
Issue Title: {issueTitle}
</Typography>
<Stack direction='row' spacing={2}>
<Typography variant="body1">
Opened: yesterday
</Typography>
<Typography variant="body1">
Priority: medium
</Typography>
</Stack>
</Stack>
</Grid>
</Grid>
</Paper>
</div>
)
}

./src/pages/App.tsx

import React, { useState } from "react"
import { Box, Typography, TextField, Stack, Button } from "@mui/material"

import ProjectCard from "components/ProjectCard"
import HelloWorld from 'components/HelloWorld'

import 'styles/App.css'


const App = (): React.JSX.Element => {
const [textInput, setTextInput] = useState('');
const handleTextInputChange = (e:any) => {
setTextInput(e.target.value);
};
return(
<div className="home_page">
<Box sx={{ml: '5rem', mr: '5rem'}}>
<HelloWorld greeting='Hello from React Typescript' />
<Typography variant="h4" sx={{textAlign: 'center'}}>
Project Issue Tracker
</Typography>
<Box sx={{
display: 'flex',
justifyContent: 'center'
}}>
<Stack spacing={2}>
<Typography variant="h5">
Add new issue
</Typography>
<TextField
id="outlined-basic"
label="Title"
variant="outlined"
onChange={handleTextInputChange}
value={textInput}
/>
<Button variant="contained">Submit</Button>
</Stack>
</Box>
<Box sx={{ml: '1rem', mt: '3rem'}}>
<Typography variant="h5" >
Opened issue
</Typography>
<ProjectCard issueTitle="Bug: Issue 1" />
<ProjectCard issueTitle="Bug: Issue 2" />
</Box>
</Box>
</div>
)
}
export default App

Redux Store

./src/redux/issueReducer.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit"

import { IssueInitialState } from 'types/interfaces'

const initialState: IssueInitialState = {
projectIssues: []
}


export const issueSlice = createSlice({
name: 'issue',
initialState,
reducers: {
addIssue: (state, action: PayloadAction<string>) => {
state.projectIssues = [...state.projectIssues, action.payload]
}
}
})


export const { addIssue } = issueSlice.actions
export default issueSlice.reducer

./src/redux/store.ts

import { configureStore } from '@reduxjs/toolkit'

import IssueReducer from "redux/issueReducer"

export const store = configureStore({
reducer: {
issue: IssueReducer
}
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

./src/main.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from "react-redux"
import { store } from 'redux/store';

import App from 'pages/App.tsx'
import 'styles/index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
)

Adding the Store

import React, { useState } from "react"
import { Box, Typography, TextField, Stack, Button } from "@mui/material"

import ProjectCard from "components/ProjectCard"
import HelloWorld from 'components/HelloWorld'

import { useDispatch, useSelector } from "react-redux"
import { RootState } from "redux/index"
import { addIssue } from "redux/issueReducer";

import 'styles/App.css'


const App = (): React.JSX.Element => {
const [textInput, setTextInput] = useState('')

const dispatch = useDispatch()
const issueList = useSelector((state: RootState) => state.issue.projectIssues)

const handleTextInputChange = (e:any) => {
setTextInput(e.target.value)
}

const handleClick = () => {
console.log(textInput)
dispatch(addIssue(textInput))
setTextInput('')
}

return(
<div className="home_page">
<Box sx={{ml: '5rem', mr: '5rem'}}>
<HelloWorld greeting='Hello from React Typescript' />
<Typography variant="h4" sx={{textAlign: 'center'}}>
Project Issue Tracker
</Typography>
<Box sx={{
display: 'flex',
justifyContent: 'center'
}}>
<Stack spacing={2}>
<Typography variant="h5">
Add new issue
</Typography>
<TextField
id="outlined-basic"
label="Title"
variant="outlined"
onChange={handleTextInputChange}
value={textInput}
/>
<Button variant="contained" onClick={handleClick}>Submit</Button>
</Stack>
</Box>
<Box sx={{ml: '1rem', mt: '3rem'}}>
<Typography variant="h5" >
Opened issue
</Typography>
{
issueList.map((issue) => {
return(
<ProjectCard key={issue} issueTitle={issue} />
)
})
}
</Box>
</Box>
</div>
)
}

export default App

AsyncThunk API Calls

./src/redux/ghIssueReducer.ts

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

import { IssuesState } from 'types/interfaces'

export const fetchIssues = createAsyncThunk<string[], void, { rejectValue: string }>(
"githubIssue/fetchIssues",
async (_, thunkAPI) => {
try {
const response = await fetch("https://api.github.com/repos/github/hub/issues");
const data = await response.json();
const issues = data.map((issue: { title: string }) => issue.title);
return issues;
} catch (error) {
return thunkAPI.rejectWithValue("Failed to fetch issues.");
}
}
)

const initialState: IssuesState = {
issues: [],
loading: false,
error: null,
}

export const issuesSliceGithub = createSlice({
name: 'github_issues',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchIssues.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchIssues.fulfilled, (state, action) => {
state.loading = false;
state.issues = action.payload;
})
.addCase(fetchIssues.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Something went wrong';
});
},
})

export default issuesSliceGithub.reducer

./src/redux/store.ts

import { configureStore } from '@reduxjs/toolkit'
import { useDispatch } from 'react-redux'

import IssueReducer from "redux/issueReducer"
import GithubIssueReducer from 'redux/ghIssueReducer'

export const store = configureStore({
reducer: {
issue: IssueReducer,
githubIssue: GithubIssueReducer
}
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()

Adding the API Call

import React, { useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
import { Box, Typography, TextField, Stack, Button } from '@mui/material'

import ProjectCard from 'components/ProjectCard'
import HelloWorld from 'components/HelloWorld'


import { useAppDispatch, RootState, AppDispatch } from 'redux/store'
import { addIssue } from 'redux/issueReducer'
import { fetchIssues } from 'redux/ghIssueReducer'

import 'styles/App.css'


const App = (): React.JSX.Element => {
const dispatch: AppDispatch = useAppDispatch()
const [textInput, setTextInput] = useState('')

const githubIssueList = useSelector((state: RootState) => state.githubIssue.issues)
const loading = useSelector((state: RootState) => state.githubIssue.loading)
const error = useSelector((state: RootState) => state.githubIssue.error)

useEffect(() => {
dispatch(fetchIssues())
}, [dispatch])

if (loading) {
return <div>Loading...</div>;
}

if (error) {
return <div>Error: {error}</div>;
}

const handleTextInputChange = (e:any) => {
setTextInput(e.target.value)
}

const handleClick = () => {
console.log(textInput)
dispatch(addIssue(textInput))
setTextInput('')
}

return(
<div className="home_page">
<Box sx={{ml: '5rem', mr: '5rem'}}>
<HelloWorld greeting='Hello from React Typescript' />
<Typography variant="h4" sx={{textAlign: 'center'}}>
Project Issue Tracker
</Typography>
<Box sx={{
display: 'flex',
justifyContent: 'center'
}}>
<Stack spacing={2}>
<Typography variant="h5">
Add new issue
</Typography>
<TextField
id="outlined-basic"
label="Title"
variant="outlined"
onChange={handleTextInputChange}
value={textInput}
/>
<Button variant="contained" onClick={handleClick}>Submit</Button>
</Stack>
</Box>
<Box sx={{ml: '1rem', mt: '3rem'}}>
<Typography variant="h5" >
Opened issue
</Typography>
{
githubIssueList?.map((issue : string) => {
return(
<ProjectCard key={issue} issueTitle={issue} />
)
})
}
</Box>
</Box>
</div>
)
}

export default App

Redux Toolkit AsyncThunk Typescript