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