|
| 1 | +# How to use react error boundaries in fable |
| 2 | + |
| 3 | +When react renders the view and encounters an error it will by default just crash the complete application. |
| 4 | +If your html page only contains a single react element your page will be essentially a white page from this point and the user needs to reload the website. |
| 5 | + |
| 6 | +The problem is that a simple `try-catch` in the view will not be enough to handle errors in the render-pipeline. |
| 7 | +In order to catch these errors you need to overwrite `componentDidCatch`, this process is documented [here](https://reactjs.org/docs/error-boundaries.html) |
| 8 | + |
| 9 | +While render-errors are rare when using fable it can happen, especially when embedding 3rd-party-components. |
| 10 | +Because of this we provide a general purpose ReactErrorBoundary component which can be added and used from your application (including elmish-architecture) |
| 11 | + |
| 12 | + |
| 13 | +## ReactErrorBoundaries.fs |
| 14 | + |
| 15 | +Just copy the following code to your project: |
| 16 | + |
| 17 | +```fsharp |
| 18 | +module ReactErrorBoundary |
| 19 | +
|
| 20 | +open Fable.Core |
| 21 | +open Fable.Import |
| 22 | +open Fable.Helpers.React |
| 23 | +
|
| 24 | +type [<AllowNullLiteral>] InfoComponentObject = |
| 25 | + abstract componentStack: string with get |
| 26 | +
|
| 27 | +[<Pojo>] |
| 28 | +type ErrorBoundaryProps = |
| 29 | + { Inner : React.ReactElement |
| 30 | + ErrorComponent : React.ReactElement |
| 31 | + OnError : exn * InfoComponentObject -> unit } |
| 32 | +
|
| 33 | +[<Pojo>] |
| 34 | +type ErrorBoundaryState = |
| 35 | + { HasErrors : bool } |
| 36 | +
|
| 37 | +// See https://github.com/MangelMaxime/Fulma/blob/master/docs/src/Widgets/Showcase.fs |
| 38 | +// See https://reactjs.org/docs/error-boundaries.html |
| 39 | +type ErrorBoundary(props) = |
| 40 | + inherit React.Component<ErrorBoundaryProps, ErrorBoundaryState>(props) |
| 41 | + do base.setInitState({ HasErrors = false }) |
| 42 | +
|
| 43 | + override x.componentDidCatch(error, info) = |
| 44 | + let info = info :?> InfoComponentObject |
| 45 | + x.props.OnError(error, info) |
| 46 | + x.setState({ HasErrors = true }) |
| 47 | +
|
| 48 | + override x.render() = |
| 49 | + if (x.state.HasErrors) then |
| 50 | + x.props.ErrorComponent |
| 51 | + else |
| 52 | + x.props.Inner |
| 53 | +
|
| 54 | +let renderCatchSimple errorElement element = |
| 55 | + ofType<ErrorBoundary,_,_> { Inner = element; ErrorComponent = errorElement; OnError = fun _ -> () } [ ] |
| 56 | +
|
| 57 | +let renderCatchFn onError errorElement element = |
| 58 | + ofType<ErrorBoundary,_,_> { Inner = element; ErrorComponent = errorElement; OnError = onError } [ ] |
| 59 | +``` |
| 60 | + |
| 61 | +## Usage |
| 62 | + |
| 63 | +Usage from an elmish application could look similar to this: |
| 64 | + |
| 65 | +Consider you have a `SubComponent` which might run into rendering issues and an `ErrorComponent` you want to show if a rendering issue occurs: |
| 66 | + |
| 67 | +```fsharp |
| 68 | +let view model dispatch = |
| 69 | + SubComponent.view model dispatch |
| 70 | + |> ReactErrorBoundary.renderCatchFn |
| 71 | + (fun (error, info) -> |
| 72 | + //dispatch (MyMessage ...) |
| 73 | + logger.Error("SubComponent failed to render" + info.componentStack, error)) |
| 74 | + (ErrorComponent.view model dispatch) |
| 75 | +``` |
| 76 | + |
| 77 | +If you don't need the `OnError` event: |
| 78 | + |
| 79 | +```fsharp |
| 80 | +let view model dispatch = |
| 81 | + SubComponent.view model dispatch |
| 82 | + |> ReactErrorBoundary.renderCatchSimple |
| 83 | + (ErrorComponent.view model dispatch) |
| 84 | +``` |
0 commit comments