Umbraco 11 + .NET 7 + React + Vite + Hooks Part 3
Tags: webdev, react, umbraco, dotnet
Published: 2023-04-08 12:06:57

in this article we will do the following

- update homepage content in umbraco and add new property 
- create a service and interface to return data from umbraco and add it to DI
- create a controller to use that service
- create custom hook to fetch data from umbraco API

##Updating our current homepage

- navigate to /umbraco and login to dashboard
- click on settings/document types > homepage
- click on add property > enter name as image, then click on select editor and select image cropper 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j85cgcfjqi3xe6pqkpyq.png)
- click on save to save the changes
- then click on settings (check image below) and select models builder to update homepage model so we can use it our service

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c28epi9qnslij7it16uj.png)
- then click on Content, update the content and click Save and publish

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8q2hrimidqmb50210c85.png)



##current folder structure without ClientApp, 

```sh 
├├─> MyCustomUmbracoProject
 │ ├─> .vscode
 │  │ ├─> launch.json
 │  │ ├─> tasks.json
 │ ├─> appsettings.json
 │ ├─> ClientApp //to be covered in bit
 │ ├─> Controllers
 │  │ ├─> HomeController.cs
 │  │ ├─> ContentController.cs
 │ ├─> Interfaces
 │  ├─> IContentService.cs
 │ ├─> Models
 │  │ ├─> GenericResult.cs
 │  │ ├─> HomepageDTO.cs
 │ ├─> Services
 │  ├─> ContentService.cs
 │ ├─> umbraco
 │  ├─> models
 │    │ ├─> Homepage.generated.cs
```

> add Interfaces, Services Folder and create the following

- under Models add 2 classes HomepageDTO, GenericResult
```cs

namespace MyCustomUmbracoProject.Models
{
    public class GenericResult<T>
    {
        public T Data { get; set; }
        public bool Success { get; set; }
        public string Message { get; set; } = null;
        public string Error { get; set; } = null;
        public IEnumerable<string> ErrorMessages { get; set; } = Enumerable.Empty<string>();
    }

}
```

```cs
namespace MyCustomUmbracoProject.Models
{
    public class HomepageDTO
    {
        public string? Title { get; set; }
        public string? ImageUrl { get; set; }
    }
}

```
- create a interface named IContentService
```cs
using MyCustomUmbracoProject.Models;

namespace MyCustomUmbracoProject.Interfaces
{
    public interface IContentService
    {
        GenericResult<HomepageDTO> GetHomeContent();

    }
}
```


- create a Service named ContentService
```cs

using MyCustomUmbracoProject.Interfaces;
using MyCustomUmbracoProject.Models;
using Umbraco.Cms.Web.Common;
using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;

namespace MyCustomUmbracoProject.Services
{
    public class ContentService : IContentService
    {
        private UmbracoHelper _umbracoHelper;
        private readonly ILogger<ContentService> _logger;

        public ContentService(ILogger<ContentService> logger, UmbracoHelper umbracoHelper)
        {
            _logger = logger;
            _umbracoHelper = umbracoHelper;

        }

        public GenericResult<HomepageDTO> GetHomeContent()
        {
            GenericResult<HomepageDTO> result = new GenericResult<HomepageDTO>();
            try
            {
                var model = this._umbracoHelper?.ContentAtRoot()?.DescendantsOrSelf<ContentModels.Homepage>().FirstOrDefault() ?? null;
                result.Data = new HomepageDTO()
                {
                    Title = model?.Title ?? "",
                    ImageUrl = model?.Image?.Src ?? ""
                };
                result.Success = true;
                result.Message = "Content Fetched Successfully";
            }
            catch (System.Exception ex)
            {
                result.Success = false;
                result.Message = "Something went wrong";

                result.Error = ex.Message;
                this._logger.LogError(ex, "error while getting data for HomeContent");
            }
            return result;
        }
    }
}
```

- inject the service and interface in DI in Startup.cs

```cs
 public void ConfigureServices(IServiceCollection services)
        {
           //Rest of code
            services.AddScoped<IContentService, ContentService>();
        }
```

- create controller named Content Controller and inject content service into constructor 

```cs
using Microsoft.AspNetCore.Mvc;
using MyCustomUmbracoProject.Models;
using MyCustomUmbracoProject.Interfaces;

namespace MyCustomUmbracoProject
{
    [Route("api/[controller]")]
    public class ContentController : ControllerBase
    {
        private IContentService _contentService;

        public ContentController(IContentService contentService)
        {
            _contentService = contentService;
        }

        [HttpGet]
        [Route("home-content")]
        public ActionResult<HomepageDTO> FetchHomeContent()
        {
            var result = this._contentService.GetHomeContent();

            return Ok(result);
        }
    }

}

```
##now for the ClientApp

```sh
├─> MyCustomUmbracoProject
 │ ├─> ClientApp
 │  │ ├─> src
 │  │  │ ├─> components
 │  │  │  ├─> Spinner.tsx
 │  │  │ ├─> hooks
 │  │  │  ├─> use-fetch.ts
 │  │  │ ├─> models
 │  │  │  │─> generic-result.ts
 │  │  │  ├─> home-page.ts

```

> create use-fetch.ts hook into hooks folder

```js
import { useCallback, useEffect, useReducer, useRef } from 'react'

interface State<T> {
    response?: T
    error?: Error
    loading: boolean;
    runQuery: (params?: Record<string, any>) => void;
}
interface FetchOptions<T> {
    url?: string;
    options?: RequestInit;
    runOnFirstRender?: boolean;
}

type Cache<T> = { [url: string]: T }

// discriminated union type
type Action<T> =
    | { type: 'loading' }
    | { type: 'fetched'; payload: T }
    | { type: 'error'; payload: Error }

const useFetch = <T = unknown>({ url, options, runOnFirstRender = true }: FetchOptions<T>): State<T> => {
    const cache = useRef<Cache<T>>({})

    // Used to prevent state update if the component is unmounted
    const cancelRequest = useRef<boolean>(false)

    const initialState: State<T> = {
        error: undefined,
        response: undefined,
        loading: false,
        runQuery: () => (params?: Record<string, any> | undefined): void => {

        }
    }

    // Keep state logic separated
    const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
        switch (action.type) {
            case 'loading':
                return { ...initialState, loading: true }
            case 'fetched':
                return { ...initialState, response: action.payload, loading: false }
            case 'error':
                return { ...initialState, error: action.payload, loading: false }
            default:
                return state
        }
    }

    const [state, dispatch] = useReducer(fetchReducer, initialState)
    const runQuery = useCallback((params?: Record<string, any>) => {
        if (url) {
            fetchData(url, params)
        }

    }, [url])
    const fetchData = async (url: string, params?: Record<string, any>) => {
        dispatch({ type: 'loading' })
      
        // If a cache exists for this url, return it
        if (cache.current[url]) {
            dispatch({ type: 'fetched', payload: cache.current[url] })
            return
        }

        try {
            const response = await fetch(url, options)
            if (!response.ok) {
                throw new Error(response.statusText ? response.statusText : response.status.toString())
            }

            const data = (await response.json()) as T
            cache.current[url] = data
            if (cancelRequest.current) return

            dispatch({ type: 'fetched', payload: data })
        } catch (error) {
            if (cancelRequest.current) return

            dispatch({ type: 'error', payload: error as Error })
        }
    }


    useEffect(() => {
        // Do nothing if the url is not given
        if (!url) return

        cancelRequest.current = false


        if (runOnFirstRender) fetchData(url, options)
        // Use the cleanup function for avoiding a possibly...
        // ...state update after the component was unmounted
        return () => {
            cancelRequest.current = true
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [url, runOnFirstRender])

    return { response: state.response, loading: state.loading, error: state.error, runQuery: runQuery }
}

export default useFetch

```
-update App.tsx

```js
import { Route, HashRouter as Router, Routes } from "react-router-dom";
import "./App.css";
import reactLogo from "./assets/react.svg";
import viteLogo from "./assets/vite.png";
import { About } from "./pages/About";
import { ContactUs } from "./pages/Contact";
import { Home } from "./pages/Home";
import { NotFound } from "./pages/NotFound";

export const App = () => {
  return (
    <>
      <nav>
        <h2 className="title">Vite + React + Umbraco</h2>
        <ul>
          <li>
            <a href="/">Home </a>
          </li>
          <li>
            <a href="#/about">About</a>
          </li>
          <li>
            <a href="#/contact">Contact</a>
          </li>
        </ul>
        
      </nav>
      <div className="App">
        <div>
          <a href="https://vitejs.dev" target="_blank">
            <img src={viteLogo} className="logo" alt="Vite logo" />
          </a>
          <a href="https://reactjs.org" target="_blank">
            <img src={reactLogo} className="logo react" alt="React logo" />
          </a>
          <a href="https://reactjs.org" target="_blank">
            <img
              src="https://user-images.githubusercontent.com/6791648/60256231-6e710c00-98d1-11e9-8120-06eecbdb858e.png"
              className="umbraco logo"
              alt="umbraco logo"
            />
          </a>
        </div>
        <Router>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/contact" element={<ContactUs />} />
            <Route path="*" element={<NotFound />} />
          </Routes>
        </Router>
        <div className="card"></div>
      </div>
    </>
  );
};

```
- update app.css

```css
* {
  padding: 0;
  margin: 0;
}

#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
  filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: no-preference) {
  a:nth-of-type(2) .logo {
    animation: logo-spin infinite 20s linear;
  }
}

.error {
  color: red;
}

/* nav start */
nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background: #cfd8dc;
}

nav ul {
  display: flex;
  list-style: none;
}

nav li {
  padding-left: 1rem;
}

nav a {
  text-decoration: none;
  color: #0d47a1;
}

/* 
  Extra small devices (phones, 600px and down) 
*/
@media only screen and (max-width: 600px) {
  nav {
    flex-direction: column;
  }
  nav ul {
    flex-direction: column;
    padding-top: 0.5rem;
  }
  nav li {
    padding: 0.5rem 0;
  }
}

.title {
  color: #242424;
}

/* To center the spinner*/
.pos-center {
  position: fixed;
  top: calc(50% - 40px);
  left: calc(50% - 40px);
}

.loader {
  border: 10px solid #f3f3f3;
  border-top: 10px solid #3498db;
  border-radius: 50%;
  width: 80px;
  height: 80px;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.center {
  display: flex;
  justify-content: center;
  align-items: center;
}

```

-update Home.tsx

```js
import useFetch from "../hooks/use-fetch";
import { HomePage } from "../models/home-page";
import { GenericResult } from "../models/generic-result";
import Spinner from "../components/Spinner";

export const Home = () => {
  const {
    response: { data, success, error: errorMsg } = {
      data: undefined,
      success: false,
      error: undefined,
    },
    error,
    loading,
  } = useFetch<GenericResult<HomePage>>({ url: "api/content/home-content" });



  return (
    <>
      <Spinner loading={loading} />
      {error && <p className="error">error ...{error?.message}</p>}
      {!loading && !success && <p className="error">error ...{errorMsg}</p>}
      {success && data && (
        <div>
          title : {data.title}
          <div>
            {data.imageUrl && (
              <img
                title={data.title}
                src={`${data.imageUrl}`}
                width="auto"
                height="200px"
              />
            )}
          </div>
        </div>
      )}
    </>
  );
};
```

##Final Output 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sh3mu8a2nz3ur399fhdi.png)

# let me know if you're interested in content like,
if would like to continue this series also let me know..

Socials:
LinkedIn: https://www.linkedin.com/in/mohammedzaky
GitHub: https://github.com/mozaky