I’m working on a small image editor side project with react app, and I wanted to sprinkle in some quick filters without pulling in a heavyweight library. Filtrr2 looked perfect old-school jQuery, a handful of filters, nothing fancy. But my very first run threw a wall of red “Unexpected token <” errors at me.
The Code That Broke
public/index.html
<body>
<div id="root"></div>
<!-- dependencies -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/camanjs/4.1.2/caman.full.js"></script>
<!-- Filtrr2 (my first, broken attempt) -->
<script src="../src/plugins/filtrr2.js"></script>
<script src="../src/plugins/util.js"></script>
<script src="../src/plugins/effects.js"></script>
<script src="../src/plugins/layers.js"></script>
<script src="../src/plugins/events.js"></script>
</body>
src/App.js
Import { useEffect } from 'react';
function App() {
useEffect(() => {
console.log('filtrr2 on window:', window.filtrr2); // <- prints undefined
}, []);
return <h1>Filtrr2 test</h1>;
}
export default App;
What I Saw in The Console
SyntaxError: Unexpected token < filtrr2.js:1
Uncaught SyntaxError: Unexpected token < util.js:1
…(same for every file)…
Why the Browser Choke
- I pointed
<script>
tags intosrc/
Create-React-App only serves the bundled output ofsrc
. Those raw files simply don’t exist at run-time. - The dev server fell back to
index.html
React’s dev server uses HTML-5 history fallback. When the browser asked for/src/plugins/filtrr2.js
, the server replied with the root HTML page. - HTML arrived where JavaScript was expected
The first character inside that page is<
. The JS parser hit it, shrugged, and threw the Unexpected token < error. window.filtrr2
stayedundefined
None of the Filtrr2 files loaded, so myconsole.log
printedundefined
.
Define Fix Code
- Move the files: I created
public/plugins
and dropped these five scripts inside:
filtrr2.js
util.js
effects.js
layers.js
events.js
- Reference them with root-relative paths:
<!-- still in public/index.html -->
<script src="%PUBLIC_URL%/plugins/filtrr2.js"></script>
<script src="%PUBLIC_URL%/plugins/util.js"></script>
<script src="%PUBLIC_URL%/plugins/effects.js"></script>
<script src="%PUBLIC_URL%/plugins/layers.js"></script>
<script src="%PUBLIC_URL%/plugins/events.js"></script>
%PUBLIC_URL%
becomes/
in dev and the correct base path in a production build.- Restart
A quicknpm start
reload, and this time the console showed the Filtrr2 constructor instead of angry red errors.
A Tiny Working Demo
I wanted something tangible, so I built a 30-line React component that:
- lets me pick an image,
- drops it on a
<canvas>
, and - toggles a grayscale filter.
// src/FiltrrDemo.js
import { useRef, useState } from 'react';
export default function FiltrrDemo() {
const canvasRef = useRef(null);
const [isGray, setIsGray] = useState(false);
/* 1 — load a file onto the canvas */
const loadImage = e => {
const file = e.target.files[0];
if (!file) return;
const img = new Image();
img.onload = () => {
const cvs = canvasRef.current;
cvs.width = img.width;
cvs.height = img.height;
cvs.getContext('2d').drawImage(img, 0, 0);
setIsGray(false);
};
img.src = URL.createObjectURL(file);
};
/* 2 — apply or remove grayscale */
const toggleGray = () => {
const cvs = canvasRef.current;
if (!cvs) return;
window.filtrr2(cvs, function () {
isGray ? this.original() : this.grayscale();
this.render(() => setIsGray(!isGray));
});
};
return (
<>
<input type="file" accept="image/*" onChange={loadImage} />
<button onClick={toggleGray} disabled={!canvasRef.current}>
{isGray ? 'Remove grayscale' : 'Apply grayscale'}
</button>
<br />
<canvas ref={canvasRef} style={{ maxWidth: '100%', marginTop: 8 }} />
</>
);
}
Drop it into App.js
:
FiltrrDemo from './FiltrrDemo';
function App() {
return (
<div style={{ padding: 16 }}>
<h1>Filtrr2–React demo</h1>
<FiltrrDemo />
</div>
);
}
export default App;
Open the page, pick a photo, smash the button instant gray scale.
Fun Ways to Keep Practising
- Stack filters —swap
grayscale()
forsepia().brightness(20).contrast(15)
. - Make it interactive —add a range slider and pipe its value into
brightness(value)
. - Save the result —
canvas.toDataURL('image/png')
and trigger a download. - One-click presets —map buttons to filter chains like
vintage
,gotham
,posterize
.
Each tweak drives home how to wrap a legacy script in a modern React workflow.
Key Lessons I’m Taking Away
- Everything inside
src/
is private until Webpack bundles it.
If you need a raw file, park it inpublic/
. - “Unexpected token <” is almost always HTML masquerading as JS.
First stop: DevTools → Network tab. - No npm? No problem.
Drop the script inpublic/
, load it early, and treat it like any other global. - Thin React wrappers keep old APIs from leaking everywhere.
My mini component isolates Filtrr2 so the rest of my app stays declarative and testable.
Final Thought
I love when a 15-year-old piece of code still finds a home in a brand-new React app it’s a reminder that the web is one giant Lego bucket. Yes, I tripped over a path error, but the fix was painless, and now I have a slick little filter playground to keep extending. If you bump into the same “Unexpected token <” monster, check your file paths, move your scripts to public/
, and keep building.